ワンクリックで
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.
// 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.
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.
Scaffold a new MCP resource definition. Use when the user asks to add a resource, expose data via URI, or create a readable endpoint.
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.
| name | add-test |
| description | 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. |
| metadata | {"author":"cyanheads","version":"1.3","audience":"external","type":"reference"} |
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
ctx features it usesbun run test to verifybun run devcheck to verify typesRead 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 |
/**
* @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.
});
});
/**
* @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');
}
});
});
/**
* @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().
For tools with task: true, use createMockContext({ progress: true }):
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);
});
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):
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).
When scaffolding tests for an existing handler, use the Zod schemas to generate meaningful test cases:
input schema — identify required fields, optional fields with defaults, constrained types (enums, min/max, patterns)output schema — know what shape to assert against.min(), .max(), .length(), test at the boundariesformat() renders uncertainty honestly (Not available, omitted badge, etc.) instead of fabricating values.tests/... or colocated with source)@fileoverview and @module header present.rejects.toThrow())format function tested if definedcreateMockContext options match handler's ctx usage (tenantId, progress, elicit, sample)beforeEach if handler depends on a service singletonformat() does not invent facts)bun run test passesbun run devcheck passes