add-test
Scaffold a test file for an existing tool, resource, or service. Use when the user asks to add tests, improve coverage, or when a definition exists without a matching test file.
Best use case
add-test is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Scaffold a test file for an existing tool, resource, or service. Use when the user asks to add tests, improve coverage, or when a definition exists without a matching test file.
Teams using add-test should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/add-test/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How add-test Compares
| Feature / Agent | add-test | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Scaffold a test file for an existing tool, resource, or service. Use when the user asks to add tests, improve coverage, or when a definition exists without a matching test file.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
## Context
Tests use Vitest and `createMockContext` from `@cyanheads/mcp-ts-core/testing`. If the repo already has tests, match the existing layout. If the repo has no existing tests, create a root `tests/` directory that mirrors the `src/` structure (e.g. `tests/mcp-server/tools/definitions/echo.tool.test.ts` for `src/mcp-server/tools/definitions/echo.tool.ts`).
For the full `createMockContext` API and testing patterns, read:
skills/api-testing/SKILL.md
## Steps
1. **Identify the target** — which tool, resource, or service needs tests
2. **Read the source file** — understand the handler's logic, input/output schemas, error paths, and which `ctx` features it uses
3. **Create the test file** in the repo's existing test layout
4. **Write test cases** covering happy path, error paths, and edge cases
5. **Run `bun run test`** to verify
6. **Run `bun run devcheck`** to verify types
## Determining What to Test
Read the handler and identify:
| Aspect | Test Strategy |
|:-------|:-------------|
| **Happy path** | Valid input → expected output. Include at least one. |
| **Input variations** | Optional fields omitted, defaults applied, boundary values |
| **Error paths** | Invalid state, missing resources, service failures → correct error thrown |
| **`ctx.state` usage** | Use `createMockContext({ tenantId: 'test' })` to enable storage |
| **`ctx.elicit` / `ctx.sample`** | Mock with `vi.fn()`, also test the absent case (undefined) |
| **`ctx.progress`** | Use `createMockContext({ progress: true })` for task tools |
| **`ctx.fail` (typed contract)** | Definitions with `errors[]` need `fail` attached to the mock ctx — `createMockContext({ errors: myTool.errors })` does it for you. Assert on `data.reason` (stable per-contract entry), not just `code`. |
| **`format` function** | Test separately if defined — it's pure, no ctx needed. Verify it renders the IDs and fields the model needs, not just a count or title. For projection-style tools, test non-default field selections. |
| **Sparse upstream payloads** | For third-party API integrations, build a fixture with omitted fields. Assert normalized output still validates and `format()` preserves unknown values instead of inventing facts. |
| **Auth scopes** | Not tested at handler level (framework enforces) — skip |
## Templates
### Tool test
```typescript
/**
* @fileoverview Tests for {{TOOL_NAME}} tool.
* @module tests/tools/{{TOOL_NAME}}.tool.test
*/
import { describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { {{TOOL_EXPORT}} } from '@/mcp-server/tools/definitions/{{tool-name}}.tool.js';
describe('{{TOOL_EXPORT}}', () => {
it('returns expected output for valid input', async () => {
const ctx = createMockContext();
const input = {{TOOL_EXPORT}}.input.parse({
// valid input matching the Zod schema
});
const result = await {{TOOL_EXPORT}}.handler(input, ctx);
expect(result).toMatchObject({
// expected output shape
});
});
it('throws on invalid state', async () => {
const ctx = createMockContext();
const input = {{TOOL_EXPORT}}.input.parse({
// input that triggers an error path
});
await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toThrow();
});
// Only when the tool declares `errors: [...]`. Drop this block otherwise.
it('throws ctx.fail("{{REASON}}") for the declared failure mode', async () => {
const ctx = createMockContext({ errors: {{TOOL_EXPORT}}.errors });
const input = {{TOOL_EXPORT}}.input.parse({
// input that triggers the declared failure mode
});
await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toMatchObject({
data: { reason: '{{REASON}}' },
});
});
it('formats output completely', () => {
const output = { /* mock output matching the output schema */ };
const blocks = {{TOOL_EXPORT}}.format!(output);
expect(blocks.some((block) => block.type === 'text')).toBe(true);
// Assert the rendered text includes the IDs/fields the LLM needs to act on.
});
});
```
### Resource test
```typescript
/**
* @fileoverview Tests for {{RESOURCE_NAME}} resource.
* @module tests/resources/{{RESOURCE_NAME}}.resource.test
*/
import { describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { {{RESOURCE_EXPORT}} } from '@/mcp-server/resources/definitions/{{resource-name}}.resource.js';
describe('{{RESOURCE_EXPORT}}', () => {
it('returns data for valid params', async () => {
const ctx = createMockContext({ tenantId: 'test-tenant' });
const params = {{RESOURCE_EXPORT}}.params.parse({
// valid params matching the Zod schema
});
const result = await {{RESOURCE_EXPORT}}.handler(params, ctx);
expect(result).toBeDefined();
});
it('throws when resource not found', async () => {
const ctx = createMockContext({ tenantId: 'test-tenant' });
const params = {{RESOURCE_EXPORT}}.params.parse({
// params for a non-existent resource
});
await expect({{RESOURCE_EXPORT}}.handler(params, ctx)).rejects.toThrow();
});
// For resources that declare an `errors: [...]` contract, pass the contract via
// `createMockContext` so the typed `ctx.fail` is wired automatically:
// const ctx = createMockContext({ errors: {{RESOURCE_EXPORT}}.errors });
// const err = await {{RESOURCE_EXPORT}}.handler(params, ctx).catch((e) => e);
// expect(err.code).toBe(JsonRpcErrorCode.NotFound);
// expect(err.data.reason).toBe('no_match');
it('lists available resources', async () => {
const listing = await {{RESOURCE_EXPORT}}.list!();
expect(listing.resources).toBeInstanceOf(Array);
expect(listing.resources.length).toBeGreaterThan(0);
for (const r of listing.resources) {
expect(r).toHaveProperty('uri');
expect(r).toHaveProperty('name');
}
});
});
```
### Service test
```typescript
/**
* @fileoverview Tests for {{SERVICE_NAME}} service.
* @module tests/services/{{domain}}/{{domain}}-service.test
*/
import { beforeEach, describe, expect, it } from 'vitest';
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
import { get{{ServiceClass}}, init{{ServiceClass}} } from '@/services/{{domain}}/{{domain}}-service.js';
import { createInMemoryStorage } from '@cyanheads/mcp-ts-core/testing';
describe('{{ServiceClass}}', () => {
beforeEach(() => {
// Re-initialize with fresh config/storage for each test
const mockStorage = createInMemoryStorage();
init{{ServiceClass}}(mockConfig, mockStorage);
});
it('performs the expected operation', async () => {
const ctx = createMockContext({ tenantId: 'test-tenant' });
const service = get{{ServiceClass}}();
const result = await service.doWork('input', ctx);
expect(result).toBeDefined();
});
});
```
If you need to test the accessor's "not initialized" guard, do it in a separate isolated-module test (`vi.resetModules()` before importing the service module). Don't mix that assertion into a suite that already calls `init{{ServiceClass}}()` in `beforeEach()`.
### Task tool test
For tools with `task: true`, use `createMockContext({ progress: true })`:
```typescript
it('reports progress during execution', async () => {
const ctx = createMockContext({ progress: true });
const input = {{TOOL_EXPORT}}.input.parse({ count: 3, delayMs: 10 });
await {{TOOL_EXPORT}}.handler(input, ctx);
const progress = ctx.progress as ContextProgress & {
_total: number;
_completed: number;
_messages: string[];
};
expect(progress._total).toBe(3);
expect(progress._completed).toBe(3);
});
it('respects cancellation', async () => {
const controller = new AbortController();
const ctx = createMockContext({ progress: true, signal: controller.signal });
const input = {{TOOL_EXPORT}}.input.parse({ count: 100, delayMs: 10 });
// Abort after a short delay
setTimeout(() => controller.abort(), 50);
const result = await {{TOOL_EXPORT}}.handler(input, ctx);
// Should have stopped early
expect(result.finalCount).toBeGreaterThan(0);
});
```
## Fuzz Testing
For schema-heavy or input-validation-critical handlers, the framework ships fuzz helpers that generate valid + adversarial inputs from your Zod schemas via `fast-check` and assert handler invariants (no crashes, no prototype pollution, no stack-trace leaks):
```typescript
import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';
it('survives fuzz testing', async () => {
const report = await fuzzTool({{TOOL_EXPORT}}, { numRuns: 100 });
expect(report.crashes).toHaveLength(0);
expect(report.leaks).toHaveLength(0);
expect(report.prototypePollution).toBe(false);
});
```
Available helpers from `@cyanheads/mcp-ts-core/testing/fuzz`: `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary` (custom property-based tests), `adversarialArbitrary` and `ADVERSARIAL_STRINGS` (targeted injection sets). Returns a `FuzzReport` you can assert against. Options: `numRuns`, `numAdversarial`, `seed` (reproducibility), `timeout`, `ctx` (`MockContextOptions` for stateful handlers).
## Generating Tests from Schemas
When scaffolding tests for an existing handler, use the Zod schemas to generate meaningful test cases:
1. **Read `input` schema** — identify required fields, optional fields with defaults, constrained types (enums, min/max, patterns)
2. **Read `output` schema** — know what shape to assert against
3. **Happy path** — construct the simplest valid input, assert output matches schema
4. **Defaults** — omit optional fields, verify defaults are applied in the output
5. **Boundaries** — if the schema has `.min()`, `.max()`, `.length()`, test at the boundaries
6. **Error paths** — trace the handler logic for throw conditions, construct inputs that trigger each
7. **Sparse upstream fixtures** — if the handler/service wraps a third-party API, add at least one fixture where upstream omits optional fields entirely. Assert that the output still validates and that `format()` renders uncertainty honestly (`Not available`, omitted badge, etc.) instead of fabricating values.
## Checklist
- [ ] Test file created in the repo's existing layout (`tests/...` or colocated with source)
- [ ] JSDoc `@fileoverview` and `@module` header present
- [ ] Happy path tested with valid input → expected output
- [ ] Error paths tested (at least one `.rejects.toThrow()`)
- [ ] `format` function tested if defined
- [ ] `createMockContext` options match handler's ctx usage (`tenantId`, `progress`, `elicit`, `sample`)
- [ ] Service re-initialized in `beforeEach` if handler depends on a service singleton
- [ ] If wrapping external API: sparse-payload case tested (omitted upstream fields still validate; `format()` does not invent facts)
- [ ] `bun run test` passes
- [ ] `bun run devcheck` passesRelated Skills
field-test
Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
api-testing
Testing patterns for MCP tool/resource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.
tool-defs-analysis
Read-only audit of MCP definition language across an existing surface — tools, resources, prompts. Walks every definition file and checks 12 categories the LLM reads to decide whether and how to call: voice & tense, internal leaks, audience leaks, defaults, recovery hints, output descriptions, cross-references, sparsity, examples, structure, mutator observability, unit-bearing numeric names. Produces grouped findings with file:line citations and a numbered options list. Use during polish, after a refactor, or before a release. Complements `field-test` (behavior testing) and `security-pass` (security audit).
setup
Post-init orientation for an MCP server built on @cyanheads/mcp-ts-core. Use after running `@cyanheads/mcp-ts-core init` to understand the project structure, conventions, and skill sync model. Also use when onboarding to an existing project for the first time.
security-pass
Review an MCP server for common security gaps: LLM-facing surfaces as injection vector (tools, resources, prompts, descriptions), scope blast radius, destructive ops without consent, upstream auth shape, input sinks (URL / path / roots / shell / sampling / schema strictness / ReDoS), tenant isolation, leakage through errors and telemetry, unbounded resources, and HTTP-mode deployment surface. Use before a release, after a batch of handler changes, or when the user asks for a security review, audit, or hardening pass. Produces grouped findings and a numbered options list.
report-issue-local
File a bug or feature request against this MCP server's own repo. Use for server-specific issues — tool logic, service integrations, config problems, or domain bugs that aren't caused by the framework.
report-issue-framework
File a bug or feature request against @cyanheads/mcp-ts-core when you hit a framework issue. Use when a builder, utility, context method, or config behaves contrary to the documented API — not for server-specific application bugs.
release-and-publish
Ship a release end-to-end across every registry the project targets (npm, MCP Registry, GitHub Releases for `.mcpb` bundles, GHCR). Runs the final verification gate, pushes commits and tags, then publishes to each applicable destination. Assumes git wrapup (version bumps, changelog, commit, annotated tag) is already complete — this skill is the post-wrapup publish workflow. Retries transient network failures on publish steps; halts with a partial-state report when retries are exhausted or the failure is terminal.
polish-docs-meta
Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
orchestrations
Pick and run a multi-phase workflow that chains foundational task skills (`git-wrapup`, `release-and-publish`, `maintenance`, `field-test`, `setup`, etc.) end-to-end. Routes user intent to a workflow file under `workflows/` — greenfield builds, maintenance + release, field-test + fix, or known-work + release. Single source for the universal rules (no commits without authorization, no destructive git, no marketing language), the orchestrator posture (own the goal, ground sub-agents in primary sources, verify against the goal), and the sub-agent strategy (orient block, parallel fanout, isolation, normalization) that apply across every workflow. Sub-agents are an optional capability — workflows run linearly when fanout isn't available.
maintenance
Investigate, adopt, and verify dependency updates — with special handling for `@cyanheads/mcp-ts-core`. Captures what changed, understands why, cross-references against the codebase, adopts framework improvements, syncs project skills, and runs final checks. Supports two entry modes: run the full flow end-to-end, or review updates you already applied.
git-wrapup
Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.