[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GVCZPY7BJVNA80Y7SV2JNJ":3,"skill-same:01M6GVCZPY7BJVNA80Y7SV2JNJ":105},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":16,"sourceHtmlUrl":17,"categories":18,"keywords":35,"publishedAt":58,"linkOnly":59,"markdownContent":60,"markdownBody":61,"frontMatter":62,"refName":68,"commitSha":69,"skillDirectory":70,"forksCount":71,"repositoryHtmlUrl":72,"repositoryHomepageUrl":6,"ownerHtmlUrl":73,"ownerBlogUrl":74,"ownerDescription":75,"updatedAt":76,"install":77,"resources":90},"01M6GVCZPY7BJVNA80Y7SV2JNJ","api-testing",null,"Testing patterns for MCP tool\u002Fresource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",81,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-api-testing\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-api-testing\u002FSKILL.md",[19,24,27,30],{"categoryNo":20,"categoryName":21,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":25,"categoryName":26,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":28,"categoryName":29,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":31,"categoryName":32,"taxonomyType":33,"taxonomyTypeDesc":34},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57],"testing","api","api testing","handler","test isolation","assertions","for","mock","isolation","conventions","options","patterns","reference","context","format","tool","vitest","testing patterns","setup","config","test","MCP","2026-11-10 10:12:30",false,"---\nname: api-testing\ndescription: >\n  Testing patterns for MCP tool\u002Fresource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.\nmetadata:\n  author: cyanheads\n  version: \"1.15\"\n  audience: external\n  type: reference\n---\n\n## Overview\n\nTests target handler behavior directly — call `handler(input, ctx)`, assert on the return value or thrown error. The framework's handler factory (try\u002Fcatch, formatting, telemetry) is not involved. Use `createMockContext` from `@cyanheads\u002Fmcp-ts-core\u002Ftesting` to construct the `ctx` argument.\n\n**Additional exports from `\u002Ftesting`:** `createMockSession()` binds a mock handler context to an HTTP session; `createFetchMock()` provides a strict upstream HTTP fake; `runToolContract()` executes a definition through schema, handler, formatting, enrichment\u002Fcontent, and production-shaped error-envelope checks. `createMockLogger()` returns a standalone `MockContextLogger`, `createInMemoryStorage(options?)` provides a real `StorageService` backed by `InMemoryProvider`, and `expectInputRequired(run)` returns the `input_required` result a multi-round-trip handler asked for (see [Mock inputs](#mock-inputs)).\n\n**Other testing subpaths:** `\u002Ftesting\u002Fvitest` (fixtures and conformance suites, below), `\u002Ftesting\u002Ffuzz` (see [Fuzz testing](#fuzz-testing)), and `\u002Ftesting\u002Fapps`, whose `renderAppTool` renders an app tool's `ui:\u002F\u002F` view against your server (a stdio command or an HTTP URL) in a headless MCP Apps host and reports initialization, errors, CSP violations, the view↔host messages, and screenshots. It needs the optional peers `@modelcontextprotocol\u002Fclient` and `@modelcontextprotocol\u002Fext-apps` plus a `chrome-headless-shell` build; the `field-test` skill covers installing one and reading the report.\n\n**Philosophy:** Test behavior, not implementation. Refactors should not break tests. Match the repo's existing test layout: fresh scaffolds use `tests\u002F`, while colocated `src\u002F**\u002F*.test.ts` files are also supported. Integration tests at I\u002FO boundaries over unit tests of internals.\n\n---\n\n## `mcpTest` — fixture-based Vitest test\n\n`mcpTest` is a `test.extend`-based Vitest test that provides `ctx`, `session`, `fetchMock`, and `storage` as **per-test fixtures** — fresh instances for every test, eliminating boilerplate and enforcing isolation automatically. `fetchMock` is installed as `globalThis.fetch` only when requested by a test and restored afterward.\n\n```ts\nimport { mcpTest } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\nmcpTest('echoes the message', async ({ ctx }) => {\n  const result = await echoTool.handler(echoTool.input.parse({ message: 'hi' }), ctx);\n  expect(result.message).toBe('hi');\n});\n\nmcpTest('uses storage fixture', async ({ ctx, storage }) => {\n  const svc = new MyService(config, storage);\n  const result = await svc.doWork(ctx);\n  expect(result).toBeDefined();\n});\n\nmcpTest('stubs an upstream HTTP boundary', async ({ fetchMock }) => {\n  fetchMock.route({\n    match: 'https:\u002F\u002Fapi.example.test\u002Fitems\u002F42',\n    respond: Response.json({ id: '42' }),\n  });\n  await expect(loadItem('42')).resolves.toMatchObject({ id: '42' });\n});\n```\n\n### Fixtures\n\n| Fixture | Type | Per-test? | Notes |\n|:--------|:-----|:----------|:------|\n| `ctx` | `Context` | Yes | Fresh `createMockContext()` each test |\n| `session` | `MockSession` | Yes | Fresh `{ sessionId, tenantId, ctx }` from `createMockSession()` |\n| `fetchMock` | `FetchMockHarness` | Yes | Strict fetch fake installed\u002Frestored around the requesting test |\n| `storage` | `StorageService` | Yes | Fresh `createInMemoryStorage()` each test |\n\n### Extending with the function form\n\nOverride fixtures using the **function form** (`async ({}, use) => { ... }`) to preserve per-test freshness. A bare-value override shares one mutable instance across the entire file — defeating the fixture's isolation guarantee.\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\n\u002F\u002F Correct — function form gives each test a fresh context:\nconst tenantTest = mcpTest.extend({\n  ctx: async ({}, use) => { await use(createMockContext({ tenantId: 'test-tenant' })); },\n});\n\n\u002F\u002F Wrong — bare value shares one ctx across every test in the file:\n\u002F\u002F const tenantTest = mcpTest.extend({ ctx: createMockContext({ tenantId: 'test-tenant' }) });\n```\n\nThe portable `\u002Ftesting` helpers are re-exported from `@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest` so fixture overrides don't need a second import.\n\n---\n\n## Upstream HTTP testing with `createFetchMock`\n\nUse the fetch harness at real outbound I\u002FO boundaries. Stub the external service, not server-owned services or handlers.\n\n```ts\nimport { createFetchMock } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst http = createFetchMock([\n  {\n    method: 'GET',\n    match: 'https:\u002F\u002Fapi.example.test\u002Fitems\u002F42',\n    respond: Response.json({ id: '42', name: 'Example' }),\n  },\n]);\n\nhttp.install();\ntry {\n  await expect(loadItem('42')).resolves.toEqual({ id: '42', name: 'Example' });\n  expect(http.calls[0]?.request.url).toBe('https:\u002F\u002Fapi.example.test\u002Fitems\u002F42');\n} finally {\n  http.restore();\n}\n```\n\nRoutes match in registration order. `match` accepts an exact URL, `RegExp`, or request predicate; `respond` accepts a static `Response` or a response factory. A static response's body is read once, on the route's first match, and every call is served a fresh `Response` over those bytes with the same `status`, `statusText`, and headers — so a consumer that cancels the body, or an error-body reader like `httpErrorFromResponse` that stops past its cap, settles on Node as on Bun. Set `once: true` for one-shot behavior. Unmatched requests throw unless `onUnhandled` is provided.\n\n**A request predicate routes on the URL's origin, never a prefix.** `req.url.startsWith(BASE_URL)` also matches a lookalike host (`https:\u002F\u002Fapi.example.test.evil.com\u002F...`), which CodeQL reports as high-severity incomplete URL substring sanitization — it scans test files as readily as `src\u002F`, so a suite that is green locally still fails the security check on a pull request. Parse the URL and compare origins, matching the path separately:\n\n```ts\nmatch: (req) => {\n  const url = new URL(req.url);\n  return url.origin === new URL(BASE_URL).origin && url.pathname.startsWith('\u002Fitems\u002F');\n},\n```\n\n---\n\n## Tool conformance with `toolContractSuite`\n\nPoint the reusable suite at a definition plus representative success and failure inputs. It checks input\u002Foutput schemas, invokes the real handler, applies formatting\u002Fenrichment\u002Fcontent, and validates both public error surfaces.\n\n```ts\nimport { JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\nimport { toolContractSuite } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\ntoolContractSuite(searchTool, {\n  success: [{ name: 'returns matches', input: { query: 'mcp' } }],\n  errors: [{\n    name: 'reports an empty query',\n    input: { query: '' },\n    code: JsonRpcErrorCode.InvalidParams,\n    reason: 'empty_query',\n  }],\n});\n```\n\nUse `runToolContract(definition, input, { context })` from `\u002Ftesting` when a custom test runner or an imperative assertion is a better fit. It intentionally skips transport auth and telemetry; those belong in transport\u002Fintegration tests.\n\nA declared reason thrown without a hint — a bare `ctx.fail('reason')` or a service throw carrying `{ reason }` — comes back with the entry's `recovery` as `data.recovery.hint` and a `Recovery:` line in `content[]`, as in production. The one production field it leaves out is `data.requestId` (and the `request \u003Cid>` term closing `content[]`), since there is no real request; a test asserting the factory's envelope instead expects both. Calling `definition.handler(...)` directly returns the `McpError` exactly as the throw site built it — no fill, no request id.\n\nArguments that fail the `input` schema are rejected the way the production handler factory rejects them: `InvalidParams` (`-32602`), with a message naming the tool and the failing fields — the first 10 issue lines, then ` (+N more)` for the rest — and `data.issues` holding Zod's first 10 issues, at most 30 in all counted through every union's branches. `issuesCount`, how many issues Zod raised at the top level, sits beside `data.issues` whenever it keeps fewer of them — past the first 10, or sooner when the 30-issue budget runs out — as `errorsCount` sits beside a cut branch list: the same bound the factory applies. `-32602` is the code a client sees on the wire, so assert it — not `ValidationError` (`-32007`), which stays the classification for a `ZodError` a handler throws itself. A result that breaks the tool's own `output` or `enrichment` schema is the definition's bug, so it returns `InternalError` (`-32603`) with a message naming that contract, exactly as in production.\n\nCancellation settles as it does in production. Pass `context: { signal }` and abort it: once the signal has fired, whatever the handler — or the output validation, `format()`, and enrichment after it — throws comes back as `RequestCancelled` (`-32011`), whether that is the signal's `AbortError`, its reason string, a `withRetry` backoff that stopped, or an `McpError` of the handler's own. A throw while the signal is still live keeps its own classification, and argument parsing stays outside the settle, so schema-invalid arguments on an aborted signal still return `InvalidParams`. A `toolContractSuite` error case with an aborted `context.signal` asserts `code: JsonRpcErrorCode.RequestCancelled` the same way.\n\n**Resources have no contract runner.** A resource definition's `handler(...)` called directly returns the throw site's `McpError` with no recovery fill, so asserting a resource's declared `errors[]` hints needs a path through the resource factory. Serve the definition from `createWorkerHandler({ name, title, resources: [def] })` (`\u002Fworker`) and `fetch` it a `resources\u002Fread` JSON-RPC request carrying the current protocol revision in both the `MCP-Protocol-Version` header and `params._meta`. The response is plain JSON or an SSE `data:` frame, and its `error` carries `code`, `data.reason`, and the filled `data.recovery.hint`.\n\n---\n\n## `createMockContext` options\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\ncreateMockContext()                                           \u002F\u002F working ctx.state on tenant 'default'\ncreateMockContext({ tenantId: 'test-tenant' })               \u002F\u002F explicit tenant scope for ctx.state\ncreateMockContext({ errors: myTool.errors })                 \u002F\u002F attaches typed ctx.fail keyed by the contract reasons\ncreateMockContext({ inputResponses: { confirm: { action: 'accept', content: { ok: true } } } }) \u002F\u002F second round of a multi-round-trip handler\ncreateMockContext({ requestState: 'opaque-state' })          \u002F\u002F seeds ctx.inputs.state()\ncreateMockContext({ clientCapabilities: { roots: {} } })     \u002F\u002F seeds ctx.clientCapabilities and filters inputResponses to declared kinds\ncreateMockContext({ requestId: 'my-id' })                    \u002F\u002F override request ID (default: 'test-request-id')\ncreateMockContext({ notifyResourceListChanged: () => {} })   \u002F\u002F with resource-list change notifier\ncreateMockContext({ notifyResourceUpdated: (_uri) => {} })   \u002F\u002F with resource update notifier\ncreateMockContext({ signal: controller.signal })             \u002F\u002F custom AbortSignal\ncreateMockContext({ auth: { clientId: 'test', scopes: [], sub: 'test-user' } }) \u002F\u002F with auth context\ncreateMockContext({ uri: new URL('myscheme:\u002F\u002Fitem\u002F123') })   \u002F\u002F for resource handler testing\n```\n\n`MockContextOptions` interface:\n\n```ts\ninterface MockContextOptions\u003CTErrors extends readonly ErrorContract[] | undefined> {\n  auth?: AuthContext;\n  clientCapabilities?: ClientCapabilities;\n  errors?: TErrors | undefined;\n  inputResponses?: InputResponses | Record\u003Cstring, unknown>;\n  notifyPromptListChanged?: () => void;\n  notifyResourceListChanged?: () => void;\n  notifyResourceUpdated?: (uri: string) => void;\n  notifyToolListChanged?: () => void;\n  requestId?: string;\n  requestState?: unknown;\n  sessionId?: string;\n  signal?: AbortSignal;\n  tenantId?: string;\n  uri?: URL;\n}\n```\n\n| Option | Effect |\n|:-------|:-------|\n| _(none)_ | Working `ctx.state` on tenant `'default'`; `ctx.inputs` is empty (first round) |\n| `auth` | Sets `ctx.auth` for scope-checking tests |\n| `clientCapabilities` | Sets `ctx.clientCapabilities` (`undefined` when omitted) and applies the production filter to `inputResponses`: only the answers these capabilities cover reach `ctx.inputs` (elicit → `elicitation`, and `elicitation.form` when it carries `content`; sampling → `sampling`, and `sampling.tools` when it holds a `tool_use` \u002F `tool_result` block; roots → `roots`). Omitted, every seeded response reaches `ctx.inputs`, so existing `{ inputResponses }` tests are unaffected. The mock's `ctx.requestInput` stays ungated either way |\n| `errors` | Attaches a typed `ctx.fail` against the contract — same wiring the production handler factory uses. Pass `myTool.errors` directly; the return type narrows to `HandlerContext\u003CReasonOf\u003C…>>`, so the context is assignable to that definition's handler parameter. |\n| `inputResponses` | Seeds `ctx.inputs` with the responses a retried request would carry, keyed by the identifiers the handler's `ctx.requestInput(...)` assigned (see below) |\n| `notifyPromptListChanged` | Assigns `ctx.notifyPromptListChanged` for prompt-list change notification tests |\n| `notifyResourceListChanged` | Assigns `ctx.notifyResourceListChanged` for resource notification tests |\n| `notifyResourceUpdated` | Assigns `ctx.notifyResourceUpdated` for resource update notification tests |\n| `notifyToolListChanged` | Assigns `ctx.notifyToolListChanged` for tool-list change notification tests |\n| `requestId` | Overrides `ctx.requestId` (default: `'test-request-id'`) |\n| `requestState` | Seeds `ctx.inputs.state()` — the opaque state a prior round attached |\n| `sessionId` | Sets `ctx.sessionId` for handlers that branch on session ID |\n| `signal` | Overrides `ctx.signal` — useful for cancellation testing |\n| `tenantId` | Scopes `ctx.state` to a specific tenant. Defaults to `'default'` — the value stdio (and HTTP with `MCP_AUTH_MODE=none`) resolves |\n| `uri` | Sets `ctx.uri` for resource handler testing |\n\n### Mock state\n\n`ctx.state` is a real `StorageService` over an `InMemoryProvider` — the production storage path, not a `Map`. A test therefore sees the same rules a deployed server enforces:\n\n- **Keys** match `^[a-zA-Z0-9_.\\-\u002F]+$` and may not contain `..`. Colons are rejected, so `cache:v1:abc` throws `McpError(ValidationError)` in the test exactly as it would in a deployment; use `cache\u002Fv1\u002Fabc`.\n- **Values** round-trip as JSON, as on every persistent provider. A read returns a fresh object in its JSON form — a `Date` reads back as its ISO string — so a test cannot pass on identity or on a `Date`\u002F`Map` surviving storage. A value JSON cannot encode (`bigint`, a cyclic reference, a top-level `undefined`, function, or symbol) rejects with `McpError(SerializationError)`.\n- **TTL** is honored. An entry written with `{ ttl: 30 }` reads back as `null` once 30 seconds elapse — drive the clock with `vi.useFakeTimers()` to assert expiry.\n- **`getMany` \u002F `setMany` \u002F `deleteMany` \u002F `list`** validate every key and prefix, and `list` paginates with the same opaque cursors.\n- **Cancellation** applies: once `ctx.signal` aborts, state operations reject.\n\n```ts\nconst ctx = createMockContext();\n\nawait ctx.state.set('cache\u002Fv1\u002Fabc', { hits: 1 }, { ttl: 30 });\nawait expect(ctx.state.get('cache\u002Fv1\u002Fabc')).resolves.toEqual({ hits: 1 });\nawait expect(ctx.state.set('cache:v1:abc', {})).rejects.toThrow(McpError);\n\nawait ctx.state.set('seen\u002Fabc', { at: new Date('2026-01-01T00:00:00Z') });\nawait expect(ctx.state.get('seen\u002Fabc')).resolves.toEqual({ at: '2026-01-01T00:00:00.000Z' });\n```\n\nReach for `createInMemoryStorage()` when a service takes a `StorageService` directly — it builds the same pair.\n\n### Mock inputs\n\n`ctx.requestInput` is the real implementation: it throws an `InputRequiredSignal` the production handler factories convert into an `input_required` result. In a unit test the handler is called directly, so that signal surfaces as a thrown value — which is exactly how you assert the first round. The examples below drive `export_report` from `api-context` § *The shape of a multi-round-trip handler*, which asks for a format the caller left out:\n\n```ts\nimport { isInputRequiredSignal } from '@cyanheads\u002Fmcp-ts-core';\n\nit('asks for the format on the first round', async () => {\n  const ctx = createMockContext();\n  await expect(exportReport.handler(exportReport.input.parse({ reportId: 'r1' }), ctx))\n    .rejects.toSatisfy(isInputRequiredSignal);\n});\n```\n\nTo assert on *what* was requested, use `expectInputRequired` from `\u002Ftesting`. It runs the handler and returns the `input_required` result the handler factory would have returned; it throws when the handler returns normally, and any other error propagates untouched:\n\n```ts\nimport { createMockContext, expectInputRequired } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst asked = await expectInputRequired(() => exportReport.handler(input, createMockContext()));\nexpect(asked.inputRequests?.format?.method).toBe('elicitation\u002Fcreate');\n```\n\nPass `asked.requestState` back as `createMockContext({ requestState })` when the handler reads state from the prior round. `inputResponses` drives the second round. `ctx.inputs.accepted(key, schema)` and `.view(key)` read it with the same helpers production uses, so a wrong response shape fails in the test:\n\n```ts\nit('exports in the format the user picked', async () => {\n  const ctx = createMockContext({\n    inputResponses: { format: { action: 'accept', content: { format: 'csv' } } },\n  });\n  await expect(exportReport.handler(input, ctx)).resolves.toMatchObject({ url: expect.any(String) });\n});\n\nit('stops when the user declines', async () => {\n  const ctx = createMockContext({\n    inputResponses: { format: { action: 'decline' } },\n  });\n  await expect(exportReport.handler(input, ctx)).rejects.toThrow(McpError);\n});\n```\n\nSeeding an answer this way is right for a handler that treats it as input. A consent gate does not: it acts only on a record it stored when it asked, so an answer seeded alone makes it ask again — see below.\n\n`ctx.inputs.dropped` is always `[]` on a mock context — the drop only happens in the SDK's wire decoding, so cover it in an integration test rather than a unit one.\n\nSeed `clientCapabilities` to test what a client without a capability gets: a pre-answered `inputResponses` the declared capabilities do not cover — a kind never declared, a form answer (one carrying `content`) from a client that declared only `elicitation.url`, a tool-use sampling answer without `sampling.tools` — never reaches `ctx.inputs`, so the handler asks again. A handler that falls through when a capability is missing (`if (ctx.clientCapabilities?.roots) … else …`) is tested by seeding both shapes.\n\nA consent gate that redeems a `ctx.state` record (see `api-context` § *Consent gates*) needs the record in the second round's storage, and each mock context has its own. Copy what round one stored into the round-two context. The record carries the caller, so seed the same `auth` (or none) on both:\n\n```ts\nconst accept = { confirm: { action: 'accept', content: { confirm: true } } };\n\nconst first = createMockContext();\nconst asked = await expectInputRequired(() => deletePath.handler(input, first));\nconst record = await first.state.get(`consent\u002F${asked.requestState}`);\n\nconst second = createMockContext({ inputResponses: accept, requestState: asked.requestState });\nawait second.state.set(`consent\u002F${asked.requestState}`, record);\nawait expect(deletePath.handler(input, second)).resolves.toEqual({ deleted: input.path });\n\n\u002F\u002F The record is spent: the same state again asks for a fresh confirmation.\nawait expect(deletePath.handler(input, second)).rejects.toSatisfy(isInputRequiredSignal);\n\n\u002F\u002F An answer with no record behind it asks as well — nothing was asked, so nothing was confirmed.\nconst unasked = createMockContext({ inputResponses: accept });\nawait expect(deletePath.handler(input, unasked)).rejects.toSatisfy(isInputRequiredSignal);\n```\n\nA record the round-two context holds under another `auth`, or one written by another tool (its `operation` differs), asks again the same way — cover whichever of those the handler's binding is meant to catch.\n\n### Mock logger\n\n`ctx.log` captures all log calls for inspection. Import `MockContextLogger` from `@cyanheads\u002Fmcp-ts-core\u002Ftesting` and cast `ctx.log` to access the `.calls` array (the cast is necessary because `createMockContext` returns `Context`, which types `log` as `ContextLogger`):\n\n```ts\nimport { createMockContext, type MockContextLogger } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst ctx = createMockContext();\nconst log = ctx.log as MockContextLogger;\n\nawait myTool.handler(input, ctx);\nexpect(log.calls.some(c => c.level === 'info' && c.msg.includes('Processing'))).toBe(true);\n```\n\n---\n\n## Full test example\n\n```ts\n\u002F\u002F tests\u002Ftools\u002Fmy-tool.tool.test.ts\nimport { describe, expect, it } from 'vitest';\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\nimport { myTool } from '@\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002Fmy-tool.tool.js';\n\ndescribe('myTool', () => {\n  it('returns expected output', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({ query: 'hello' });\n    const result = await myTool.handler(input, ctx);\n    expect(result.result).toBe('Found: hello');\n  });\n\n  it('throws on invalid state', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({ query: 'TRIGGER_ERROR' });\n    await expect(myTool.handler(input, ctx)).rejects.toThrow();\n  });\n\n  it('formats response completely', () => {\n    const result = { result: 'test' };\n    const blocks = myTool.format!(result);\n    expect(blocks[0].type).toBe('text');\n    expect((blocks[0] as { text?: string }).text).toContain('test');\n  });\n});\n```\n\nParse input through `myTool.input.parse(...)` to validate against the Zod schema and produce the typed input the handler expects. Call `myTool.handler(input, ctx)` directly, not through the MCP SDK or any framework wrapper. Assert on the return value for happy paths; use `.rejects.toThrow()` for error paths. Test `format` separately if the tool defines one — it's a pure function and needs no `ctx`. Verify the rendered text includes the fields the LLM needs, and for projection-style tools, add a case with non-default field selections.\n\n---\n\n## Testing with form-based client payloads\n\nLLM clients only send populated fields. **Form-based clients** (MCP Inspector, web UIs) submit the full schema shape — optional object fields arrive with empty-string inner values instead of `undefined`. Both are valid MCP usage. Test that handlers handle both gracefully.\n\n```ts\ndescribe('form-client payloads', () => {\n  it('skips optional object when inner fields are empty strings', async () => {\n    const ctx = createMockContext();\n    \u002F\u002F Form client sends the object with empty values instead of omitting it\n    const input = myTool.input.parse({\n      query: 'test',\n      dateRange: { minDate: '', maxDate: '' },\n    });\n    const result = await myTool.handler(input, ctx);\n    \u002F\u002F Should succeed — empty dateRange is ignored, not passed downstream\n    expect(result.items).toBeDefined();\n  });\n\n  it('uses optional object when inner fields have real values', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({\n      query: 'test',\n      dateRange: { minDate: '2025-01-01', maxDate: '2025-12-31' },\n    });\n    const result = await myTool.handler(input, ctx);\n    \u002F\u002F Should apply the date filter\n    expect(result.items).toBeDefined();\n  });\n});\n```\n\nThe pattern: parse through the schema (confirms Zod accepts the payload), call the handler, assert the empty-value case produces correct results — no errors, no corrupted downstream queries. Same applies to optional arrays: test with `[]` to verify the handler skips rather than passes through.\n\n---\n\n## Testing with sparse upstream payloads\n\nThis is a different problem from form-client `''` payloads. Here the upstream API omits fields entirely. The risk is either a validation failure from an over-strict schema or a quiet lie where missing data turns into a concrete fact.\n\n```ts\ndescribe('sparse upstream payloads', () => {\n  it('preserves missing upstream fields as unknown', async () => {\n    const upstream = {\n      id: 'repo-123',\n      name: 'Widget Repo',\n      \u002F\u002F archived and star_count omitted entirely\n    };\n\n    const normalized = normalizeRepo(upstream);\n    expect(normalized).toEqual({\n      id: 'repo-123',\n      name: 'Widget Repo',\n    });\n\n    const output = repoSearchTool.output.parse({\n      repos: [normalized],\n    });\n    const blocks = repoSearchTool.format!(output);\n    expect((blocks[0] as { text: string }).text).toContain('Archived:** Not available');\n    expect((blocks[0] as { text: string }).text).not.toContain('Archived:** No');\n  });\n});\n```\n\n**What to verify:**\n\n- Fixtures omit fields entirely, not just set them to `null` or `''`.\n- Normalization\u002Fhelpers tolerate missing fields without fabricating defaults.\n- Handler output still validates against the declared output schema.\n- `format()` uses explicit unknown-state fallbacks instead of inventing facts.\n- Tool-semantic defaults are tested separately from upstream absence so the distinction stays clear.\n\n---\n\n## Vitest config\n\nExtend the framework's base config using `mergeConfig`. The base provides `globals: true`, `pool: 'forks'`, `isolate: true`, `tsconfigPaths`, and a Zod SSR compatibility fix. Add only the `@\u002F` alias for your server's source:\n\n```ts\n\u002F\u002F vitest.config.ts\nimport { defineConfig, mergeConfig } from 'vitest\u002Fconfig';\nimport coreConfig from '@cyanheads\u002Fmcp-ts-core\u002Fvitest.config';\n\nexport default mergeConfig(coreConfig, defineConfig({\n  resolve: {\n    alias: { '@\u002F': new URL('.\u002Fsrc\u002F', import.meta.url).pathname },\n  },\n}));\n```\n\n`mergeConfig` deep-merges the framework base with your overrides. The base sets `globals: true` (`describe`, `it`, `expect`, etc. available without imports), `pool: 'forks'` and `isolate: true` (test files run in separate worker processes), and `ssr: { noExternal: ['zod'] }` for Zod 4 compatibility. The `resolve.alias` entry maps `@\u002F` to `src\u002F`, matching the `paths` alias in `tsconfig.json` so imports like `@\u002Fservices\u002F...` resolve correctly in tests.\n\n---\n\n## Test isolation\n\n**Construct dependencies fresh in `beforeEach`.** Never share mutable state across tests.\n\n```ts\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { initMyService } from '@\u002Fservices\u002Fmy-domain\u002Fmy-service.js';\n\ndescribe('myTool with service', () => {\n  beforeEach(() => {\n    \u002F\u002F Re-initialize with a fresh instance before each test\n    initMyService(mockConfig, mockStorage);\n  });\n\n  it('calls service correctly', async () => {\n    const ctx = createMockContext({ tenantId: 'test-tenant' });\n    \u002F\u002F ...\n  });\n});\n```\n\n- Re-init services with `initMyService()` (or equivalent) in `beforeEach` when tests share a module-level singleton.\n- Vitest runs test files in separate workers — parallel file execution is safe by default.\n- Pass `createMockContext({ tenantId })` when a test needs a specific tenant; omitting it scopes state to `'default'`, not to a broken state surface.\n\n---\n\n## McpError assertions\n\n```ts\nimport { McpError, JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\n\nit('throws NotFound for missing resource', async () => {\n  const ctx = createMockContext();\n  const input = myTool.input.parse({ id: 'nonexistent' });\n  await expect(myTool.handler(input, ctx)).rejects.toMatchObject({\n    code: JsonRpcErrorCode.NotFound,\n  });\n});\n```\n\nUse `.rejects.toThrow(McpError)` to assert type only. Use `.rejects.toMatchObject({ code: ... })` when the specific error code matters.\n\n---\n\n## Output schema assertions\n\n`expect.schemaMatching` (Vitest 4, Standard Schema) validates a value against any Zod schema — including the definition's own `output`. Use it to assert schema conformance without duplicating the shape in the test:\n\n```ts\nit('output conforms to the declared output schema', async () => {\n  const ctx = createMockContext();\n  const result = await myTool.handler(myTool.input.parse({ query: 'x' }), ctx);\n  expect(result).toEqual(expect.schemaMatching(myTool.output));\n});\n```\n\nIt composes as an asymmetric matcher anywhere a value is expected — e.g. `toHaveBeenCalledWith(expect.schemaMatching(schema))`. Prefer exact-value assertions when the expected output is fully known; reach for `schemaMatching` when the output is dynamic (timestamps, generated IDs) or the schema itself is the contract under test.\n\n---\n\n## Testing handlers with `errors[]` (typed contract)\n\nTools and resources that declare an `errors[]` contract receive a typed `ctx.fail` helper at runtime. Pass the definition's own `errors` to `createMockContext` and the mock wires `fail` the same way the production handler factory does:\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\nimport { JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\nimport { fetchItems } from '@\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002Ffetch-items.tool.js';\n\nit('throws ctx.fail(\"no_match\") when no items resolve', async () => {\n  const ctx = createMockContext({ errors: fetchItems.errors });\n\n  const input = fetchItems.input.parse({ ids: ['missing'] });\n  await expect(fetchItems.handler(input, ctx)).rejects.toMatchObject({\n    code: JsonRpcErrorCode.NotFound,\n    data: { reason: 'no_match' },\n  });\n});\n```\n\nFor lower-level tests that need the raw `fail` helper without a full mock context (e.g. asserting the reason → code mapping), use `createFail` directly — see [Testing the handler-side `fail` plumbing](#testing-the-handler-side-fail-plumbing) below.\n\n### Why test `data.reason` and not just `code`?\n\nThe contract reason is the stable machine-readable identifier — clients switch on it the same way they would on an HTTP status. A code alone (`NotFound`) doesn't disambiguate between contract entries that share a code (`'no_match'` vs `'withdrawn'` both mapping to `NotFound`). Asserting on `data.reason` locks the test to the specific contract entry.\n\n### `data.reason` is overridable-proof\n\nThe framework spreads caller-supplied data first and writes `reason` last, so a handler that passes `data: { reason: 'something_else' }` cannot override the contract reason. Tests can rely on `data.reason` always equaling the contract entry's reason — write assertions that depend on it without paranoia.\n\n### Testing the handler-side `fail` plumbing\n\nTo verify the definition wires `ctx.fail` correctly without exercising the full handler factory, use the `errors` array directly:\n\n```ts\nimport { createFail } from '@cyanheads\u002Fmcp-ts-core';\n\nit('builds an error with the contract code and reason', () => {\n  const fail = createFail(myTool.errors!);\n  const err = fail('no_match', 'not found', { itemId: '123' });\n  expect(err.code).toBe(JsonRpcErrorCode.NotFound);\n  expect(err.data).toEqual({ reason: 'no_match', itemId: '123' });\n});\n```\n\n---\n\n## Fuzz testing\n\nFor schema-heavy or input-validation-critical handlers, the framework ships fuzz helpers under `@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Ffuzz`. They generate valid + adversarial inputs from your Zod schemas via `fast-check` and assert handler invariants (no crashes, no prototype pollution, no stack-trace leaks).\n\n```ts\nimport { fuzzTool, fuzzResource, fuzzPrompt } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Ffuzz';\n\nit('survives fuzz testing', async () => {\n  const report = await fuzzTool(myTool, { numRuns: 100, numAdversarial: 30 });\n  expect(report.crashes).toHaveLength(0);\n  expect(report.leaks).toHaveLength(0);\n  expect(report.prototypePollution).toBe(false);\n});\n```\n\n| Helper | Purpose |\n|:-------|:--------|\n| `fuzzTool(def, opts)` \u002F `fuzzResource(def, opts)` \u002F `fuzzPrompt(def, opts)` | Drive valid + adversarial inputs through the handler. Returns a `FuzzReport`. |\n| `zodToArbitrary(schema)` | Convert a Zod schema to a `fast-check` `Arbitrary` for custom property-based tests. |\n| `adversarialArbitrary()` \u002F `ADVERSARIAL_STRINGS` | Targeted injection sets (prototype pollution probes, control characters, oversized payloads). |\n\n`FuzzOptions`: `numRuns` (default 50), `numAdversarial` (default 30), `seed` (reproducibility), `timeout` (per-call ms, default 5000), `ctx` (`MockContextOptions` for stateful handlers).\n\n`report.leaks` looks for a stack frame or a server-side path in what a client can observe — the `code`, `message`, and `data` of the thrown `McpError`. Strings the input itself supplied are removed before that check, so naming the offending value in error data (`throw validationError(msg, { key })`) never registers as a leak: the client sent those bytes and learns nothing from seeing them again.\n","## Overview\n\nTests target handler behavior directly — call `handler(input, ctx)`, assert on the return value or thrown error. The framework's handler factory (try\u002Fcatch, formatting, telemetry) is not involved. Use `createMockContext` from `@cyanheads\u002Fmcp-ts-core\u002Ftesting` to construct the `ctx` argument.\n\n**Additional exports from `\u002Ftesting`:** `createMockSession()` binds a mock handler context to an HTTP session; `createFetchMock()` provides a strict upstream HTTP fake; `runToolContract()` executes a definition through schema, handler, formatting, enrichment\u002Fcontent, and production-shaped error-envelope checks. `createMockLogger()` returns a standalone `MockContextLogger`, `createInMemoryStorage(options?)` provides a real `StorageService` backed by `InMemoryProvider`, and `expectInputRequired(run)` returns the `input_required` result a multi-round-trip handler asked for (see [Mock inputs](#mock-inputs)).\n\n**Other testing subpaths:** `\u002Ftesting\u002Fvitest` (fixtures and conformance suites, below), `\u002Ftesting\u002Ffuzz` (see [Fuzz testing](#fuzz-testing)), and `\u002Ftesting\u002Fapps`, whose `renderAppTool` renders an app tool's `ui:\u002F\u002F` view against your server (a stdio command or an HTTP URL) in a headless MCP Apps host and reports initialization, errors, CSP violations, the view↔host messages, and screenshots. It needs the optional peers `@modelcontextprotocol\u002Fclient` and `@modelcontextprotocol\u002Fext-apps` plus a `chrome-headless-shell` build; the `field-test` skill covers installing one and reading the report.\n\n**Philosophy:** Test behavior, not implementation. Refactors should not break tests. Match the repo's existing test layout: fresh scaffolds use `tests\u002F`, while colocated `src\u002F**\u002F*.test.ts` files are also supported. Integration tests at I\u002FO boundaries over unit tests of internals.\n\n---\n\n## `mcpTest` — fixture-based Vitest test\n\n`mcpTest` is a `test.extend`-based Vitest test that provides `ctx`, `session`, `fetchMock`, and `storage` as **per-test fixtures** — fresh instances for every test, eliminating boilerplate and enforcing isolation automatically. `fetchMock` is installed as `globalThis.fetch` only when requested by a test and restored afterward.\n\n```ts\nimport { mcpTest } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\nmcpTest('echoes the message', async ({ ctx }) => {\n  const result = await echoTool.handler(echoTool.input.parse({ message: 'hi' }), ctx);\n  expect(result.message).toBe('hi');\n});\n\nmcpTest('uses storage fixture', async ({ ctx, storage }) => {\n  const svc = new MyService(config, storage);\n  const result = await svc.doWork(ctx);\n  expect(result).toBeDefined();\n});\n\nmcpTest('stubs an upstream HTTP boundary', async ({ fetchMock }) => {\n  fetchMock.route({\n    match: 'https:\u002F\u002Fapi.example.test\u002Fitems\u002F42',\n    respond: Response.json({ id: '42' }),\n  });\n  await expect(loadItem('42')).resolves.toMatchObject({ id: '42' });\n});\n```\n\n### Fixtures\n\n| Fixture | Type | Per-test? | Notes |\n|:--------|:-----|:----------|:------|\n| `ctx` | `Context` | Yes | Fresh `createMockContext()` each test |\n| `session` | `MockSession` | Yes | Fresh `{ sessionId, tenantId, ctx }` from `createMockSession()` |\n| `fetchMock` | `FetchMockHarness` | Yes | Strict fetch fake installed\u002Frestored around the requesting test |\n| `storage` | `StorageService` | Yes | Fresh `createInMemoryStorage()` each test |\n\n### Extending with the function form\n\nOverride fixtures using the **function form** (`async ({}, use) => { ... }`) to preserve per-test freshness. A bare-value override shares one mutable instance across the entire file — defeating the fixture's isolation guarantee.\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\n\u002F\u002F Correct — function form gives each test a fresh context:\nconst tenantTest = mcpTest.extend({\n  ctx: async ({}, use) => { await use(createMockContext({ tenantId: 'test-tenant' })); },\n});\n\n\u002F\u002F Wrong — bare value shares one ctx across every test in the file:\n\u002F\u002F const tenantTest = mcpTest.extend({ ctx: createMockContext({ tenantId: 'test-tenant' }) });\n```\n\nThe portable `\u002Ftesting` helpers are re-exported from `@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest` so fixture overrides don't need a second import.\n\n---\n\n## Upstream HTTP testing with `createFetchMock`\n\nUse the fetch harness at real outbound I\u002FO boundaries. Stub the external service, not server-owned services or handlers.\n\n```ts\nimport { createFetchMock } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst http = createFetchMock([\n  {\n    method: 'GET',\n    match: 'https:\u002F\u002Fapi.example.test\u002Fitems\u002F42',\n    respond: Response.json({ id: '42', name: 'Example' }),\n  },\n]);\n\nhttp.install();\ntry {\n  await expect(loadItem('42')).resolves.toEqual({ id: '42', name: 'Example' });\n  expect(http.calls[0]?.request.url).toBe('https:\u002F\u002Fapi.example.test\u002Fitems\u002F42');\n} finally {\n  http.restore();\n}\n```\n\nRoutes match in registration order. `match` accepts an exact URL, `RegExp`, or request predicate; `respond` accepts a static `Response` or a response factory. A static response's body is read once, on the route's first match, and every call is served a fresh `Response` over those bytes with the same `status`, `statusText`, and headers — so a consumer that cancels the body, or an error-body reader like `httpErrorFromResponse` that stops past its cap, settles on Node as on Bun. Set `once: true` for one-shot behavior. Unmatched requests throw unless `onUnhandled` is provided.\n\n**A request predicate routes on the URL's origin, never a prefix.** `req.url.startsWith(BASE_URL)` also matches a lookalike host (`https:\u002F\u002Fapi.example.test.evil.com\u002F...`), which CodeQL reports as high-severity incomplete URL substring sanitization — it scans test files as readily as `src\u002F`, so a suite that is green locally still fails the security check on a pull request. Parse the URL and compare origins, matching the path separately:\n\n```ts\nmatch: (req) => {\n  const url = new URL(req.url);\n  return url.origin === new URL(BASE_URL).origin && url.pathname.startsWith('\u002Fitems\u002F');\n},\n```\n\n---\n\n## Tool conformance with `toolContractSuite`\n\nPoint the reusable suite at a definition plus representative success and failure inputs. It checks input\u002Foutput schemas, invokes the real handler, applies formatting\u002Fenrichment\u002Fcontent, and validates both public error surfaces.\n\n```ts\nimport { JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\nimport { toolContractSuite } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Fvitest';\n\ntoolContractSuite(searchTool, {\n  success: [{ name: 'returns matches', input: { query: 'mcp' } }],\n  errors: [{\n    name: 'reports an empty query',\n    input: { query: '' },\n    code: JsonRpcErrorCode.InvalidParams,\n    reason: 'empty_query',\n  }],\n});\n```\n\nUse `runToolContract(definition, input, { context })` from `\u002Ftesting` when a custom test runner or an imperative assertion is a better fit. It intentionally skips transport auth and telemetry; those belong in transport\u002Fintegration tests.\n\nA declared reason thrown without a hint — a bare `ctx.fail('reason')` or a service throw carrying `{ reason }` — comes back with the entry's `recovery` as `data.recovery.hint` and a `Recovery:` line in `content[]`, as in production. The one production field it leaves out is `data.requestId` (and the `request \u003Cid>` term closing `content[]`), since there is no real request; a test asserting the factory's envelope instead expects both. Calling `definition.handler(...)` directly returns the `McpError` exactly as the throw site built it — no fill, no request id.\n\nArguments that fail the `input` schema are rejected the way the production handler factory rejects them: `InvalidParams` (`-32602`), with a message naming the tool and the failing fields — the first 10 issue lines, then ` (+N more)` for the rest — and `data.issues` holding Zod's first 10 issues, at most 30 in all counted through every union's branches. `issuesCount`, how many issues Zod raised at the top level, sits beside `data.issues` whenever it keeps fewer of them — past the first 10, or sooner when the 30-issue budget runs out — as `errorsCount` sits beside a cut branch list: the same bound the factory applies. `-32602` is the code a client sees on the wire, so assert it — not `ValidationError` (`-32007`), which stays the classification for a `ZodError` a handler throws itself. A result that breaks the tool's own `output` or `enrichment` schema is the definition's bug, so it returns `InternalError` (`-32603`) with a message naming that contract, exactly as in production.\n\nCancellation settles as it does in production. Pass `context: { signal }` and abort it: once the signal has fired, whatever the handler — or the output validation, `format()`, and enrichment after it — throws comes back as `RequestCancelled` (`-32011`), whether that is the signal's `AbortError`, its reason string, a `withRetry` backoff that stopped, or an `McpError` of the handler's own. A throw while the signal is still live keeps its own classification, and argument parsing stays outside the settle, so schema-invalid arguments on an aborted signal still return `InvalidParams`. A `toolContractSuite` error case with an aborted `context.signal` asserts `code: JsonRpcErrorCode.RequestCancelled` the same way.\n\n**Resources have no contract runner.** A resource definition's `handler(...)` called directly returns the throw site's `McpError` with no recovery fill, so asserting a resource's declared `errors[]` hints needs a path through the resource factory. Serve the definition from `createWorkerHandler({ name, title, resources: [def] })` (`\u002Fworker`) and `fetch` it a `resources\u002Fread` JSON-RPC request carrying the current protocol revision in both the `MCP-Protocol-Version` header and `params._meta`. The response is plain JSON or an SSE `data:` frame, and its `error` carries `code`, `data.reason`, and the filled `data.recovery.hint`.\n\n---\n\n## `createMockContext` options\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\ncreateMockContext()                                           \u002F\u002F working ctx.state on tenant 'default'\ncreateMockContext({ tenantId: 'test-tenant' })               \u002F\u002F explicit tenant scope for ctx.state\ncreateMockContext({ errors: myTool.errors })                 \u002F\u002F attaches typed ctx.fail keyed by the contract reasons\ncreateMockContext({ inputResponses: { confirm: { action: 'accept', content: { ok: true } } } }) \u002F\u002F second round of a multi-round-trip handler\ncreateMockContext({ requestState: 'opaque-state' })          \u002F\u002F seeds ctx.inputs.state()\ncreateMockContext({ clientCapabilities: { roots: {} } })     \u002F\u002F seeds ctx.clientCapabilities and filters inputResponses to declared kinds\ncreateMockContext({ requestId: 'my-id' })                    \u002F\u002F override request ID (default: 'test-request-id')\ncreateMockContext({ notifyResourceListChanged: () => {} })   \u002F\u002F with resource-list change notifier\ncreateMockContext({ notifyResourceUpdated: (_uri) => {} })   \u002F\u002F with resource update notifier\ncreateMockContext({ signal: controller.signal })             \u002F\u002F custom AbortSignal\ncreateMockContext({ auth: { clientId: 'test', scopes: [], sub: 'test-user' } }) \u002F\u002F with auth context\ncreateMockContext({ uri: new URL('myscheme:\u002F\u002Fitem\u002F123') })   \u002F\u002F for resource handler testing\n```\n\n`MockContextOptions` interface:\n\n```ts\ninterface MockContextOptions\u003CTErrors extends readonly ErrorContract[] | undefined> {\n  auth?: AuthContext;\n  clientCapabilities?: ClientCapabilities;\n  errors?: TErrors | undefined;\n  inputResponses?: InputResponses | Record\u003Cstring, unknown>;\n  notifyPromptListChanged?: () => void;\n  notifyResourceListChanged?: () => void;\n  notifyResourceUpdated?: (uri: string) => void;\n  notifyToolListChanged?: () => void;\n  requestId?: string;\n  requestState?: unknown;\n  sessionId?: string;\n  signal?: AbortSignal;\n  tenantId?: string;\n  uri?: URL;\n}\n```\n\n| Option | Effect |\n|:-------|:-------|\n| _(none)_ | Working `ctx.state` on tenant `'default'`; `ctx.inputs` is empty (first round) |\n| `auth` | Sets `ctx.auth` for scope-checking tests |\n| `clientCapabilities` | Sets `ctx.clientCapabilities` (`undefined` when omitted) and applies the production filter to `inputResponses`: only the answers these capabilities cover reach `ctx.inputs` (elicit → `elicitation`, and `elicitation.form` when it carries `content`; sampling → `sampling`, and `sampling.tools` when it holds a `tool_use` \u002F `tool_result` block; roots → `roots`). Omitted, every seeded response reaches `ctx.inputs`, so existing `{ inputResponses }` tests are unaffected. The mock's `ctx.requestInput` stays ungated either way |\n| `errors` | Attaches a typed `ctx.fail` against the contract — same wiring the production handler factory uses. Pass `myTool.errors` directly; the return type narrows to `HandlerContext\u003CReasonOf\u003C…>>`, so the context is assignable to that definition's handler parameter. |\n| `inputResponses` | Seeds `ctx.inputs` with the responses a retried request would carry, keyed by the identifiers the handler's `ctx.requestInput(...)` assigned (see below) |\n| `notifyPromptListChanged` | Assigns `ctx.notifyPromptListChanged` for prompt-list change notification tests |\n| `notifyResourceListChanged` | Assigns `ctx.notifyResourceListChanged` for resource notification tests |\n| `notifyResourceUpdated` | Assigns `ctx.notifyResourceUpdated` for resource update notification tests |\n| `notifyToolListChanged` | Assigns `ctx.notifyToolListChanged` for tool-list change notification tests |\n| `requestId` | Overrides `ctx.requestId` (default: `'test-request-id'`) |\n| `requestState` | Seeds `ctx.inputs.state()` — the opaque state a prior round attached |\n| `sessionId` | Sets `ctx.sessionId` for handlers that branch on session ID |\n| `signal` | Overrides `ctx.signal` — useful for cancellation testing |\n| `tenantId` | Scopes `ctx.state` to a specific tenant. Defaults to `'default'` — the value stdio (and HTTP with `MCP_AUTH_MODE=none`) resolves |\n| `uri` | Sets `ctx.uri` for resource handler testing |\n\n### Mock state\n\n`ctx.state` is a real `StorageService` over an `InMemoryProvider` — the production storage path, not a `Map`. A test therefore sees the same rules a deployed server enforces:\n\n- **Keys** match `^[a-zA-Z0-9_.\\-\u002F]+$` and may not contain `..`. Colons are rejected, so `cache:v1:abc` throws `McpError(ValidationError)` in the test exactly as it would in a deployment; use `cache\u002Fv1\u002Fabc`.\n- **Values** round-trip as JSON, as on every persistent provider. A read returns a fresh object in its JSON form — a `Date` reads back as its ISO string — so a test cannot pass on identity or on a `Date`\u002F`Map` surviving storage. A value JSON cannot encode (`bigint`, a cyclic reference, a top-level `undefined`, function, or symbol) rejects with `McpError(SerializationError)`.\n- **TTL** is honored. An entry written with `{ ttl: 30 }` reads back as `null` once 30 seconds elapse — drive the clock with `vi.useFakeTimers()` to assert expiry.\n- **`getMany` \u002F `setMany` \u002F `deleteMany` \u002F `list`** validate every key and prefix, and `list` paginates with the same opaque cursors.\n- **Cancellation** applies: once `ctx.signal` aborts, state operations reject.\n\n```ts\nconst ctx = createMockContext();\n\nawait ctx.state.set('cache\u002Fv1\u002Fabc', { hits: 1 }, { ttl: 30 });\nawait expect(ctx.state.get('cache\u002Fv1\u002Fabc')).resolves.toEqual({ hits: 1 });\nawait expect(ctx.state.set('cache:v1:abc', {})).rejects.toThrow(McpError);\n\nawait ctx.state.set('seen\u002Fabc', { at: new Date('2026-01-01T00:00:00Z') });\nawait expect(ctx.state.get('seen\u002Fabc')).resolves.toEqual({ at: '2026-01-01T00:00:00.000Z' });\n```\n\nReach for `createInMemoryStorage()` when a service takes a `StorageService` directly — it builds the same pair.\n\n### Mock inputs\n\n`ctx.requestInput` is the real implementation: it throws an `InputRequiredSignal` the production handler factories convert into an `input_required` result. In a unit test the handler is called directly, so that signal surfaces as a thrown value — which is exactly how you assert the first round. The examples below drive `export_report` from `api-context` § *The shape of a multi-round-trip handler*, which asks for a format the caller left out:\n\n```ts\nimport { isInputRequiredSignal } from '@cyanheads\u002Fmcp-ts-core';\n\nit('asks for the format on the first round', async () => {\n  const ctx = createMockContext();\n  await expect(exportReport.handler(exportReport.input.parse({ reportId: 'r1' }), ctx))\n    .rejects.toSatisfy(isInputRequiredSignal);\n});\n```\n\nTo assert on *what* was requested, use `expectInputRequired` from `\u002Ftesting`. It runs the handler and returns the `input_required` result the handler factory would have returned; it throws when the handler returns normally, and any other error propagates untouched:\n\n```ts\nimport { createMockContext, expectInputRequired } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst asked = await expectInputRequired(() => exportReport.handler(input, createMockContext()));\nexpect(asked.inputRequests?.format?.method).toBe('elicitation\u002Fcreate');\n```\n\nPass `asked.requestState` back as `createMockContext({ requestState })` when the handler reads state from the prior round. `inputResponses` drives the second round. `ctx.inputs.accepted(key, schema)` and `.view(key)` read it with the same helpers production uses, so a wrong response shape fails in the test:\n\n```ts\nit('exports in the format the user picked', async () => {\n  const ctx = createMockContext({\n    inputResponses: { format: { action: 'accept', content: { format: 'csv' } } },\n  });\n  await expect(exportReport.handler(input, ctx)).resolves.toMatchObject({ url: expect.any(String) });\n});\n\nit('stops when the user declines', async () => {\n  const ctx = createMockContext({\n    inputResponses: { format: { action: 'decline' } },\n  });\n  await expect(exportReport.handler(input, ctx)).rejects.toThrow(McpError);\n});\n```\n\nSeeding an answer this way is right for a handler that treats it as input. A consent gate does not: it acts only on a record it stored when it asked, so an answer seeded alone makes it ask again — see below.\n\n`ctx.inputs.dropped` is always `[]` on a mock context — the drop only happens in the SDK's wire decoding, so cover it in an integration test rather than a unit one.\n\nSeed `clientCapabilities` to test what a client without a capability gets: a pre-answered `inputResponses` the declared capabilities do not cover — a kind never declared, a form answer (one carrying `content`) from a client that declared only `elicitation.url`, a tool-use sampling answer without `sampling.tools` — never reaches `ctx.inputs`, so the handler asks again. A handler that falls through when a capability is missing (`if (ctx.clientCapabilities?.roots) … else …`) is tested by seeding both shapes.\n\nA consent gate that redeems a `ctx.state` record (see `api-context` § *Consent gates*) needs the record in the second round's storage, and each mock context has its own. Copy what round one stored into the round-two context. The record carries the caller, so seed the same `auth` (or none) on both:\n\n```ts\nconst accept = { confirm: { action: 'accept', content: { confirm: true } } };\n\nconst first = createMockContext();\nconst asked = await expectInputRequired(() => deletePath.handler(input, first));\nconst record = await first.state.get(`consent\u002F${asked.requestState}`);\n\nconst second = createMockContext({ inputResponses: accept, requestState: asked.requestState });\nawait second.state.set(`consent\u002F${asked.requestState}`, record);\nawait expect(deletePath.handler(input, second)).resolves.toEqual({ deleted: input.path });\n\n\u002F\u002F The record is spent: the same state again asks for a fresh confirmation.\nawait expect(deletePath.handler(input, second)).rejects.toSatisfy(isInputRequiredSignal);\n\n\u002F\u002F An answer with no record behind it asks as well — nothing was asked, so nothing was confirmed.\nconst unasked = createMockContext({ inputResponses: accept });\nawait expect(deletePath.handler(input, unasked)).rejects.toSatisfy(isInputRequiredSignal);\n```\n\nA record the round-two context holds under another `auth`, or one written by another tool (its `operation` differs), asks again the same way — cover whichever of those the handler's binding is meant to catch.\n\n### Mock logger\n\n`ctx.log` captures all log calls for inspection. Import `MockContextLogger` from `@cyanheads\u002Fmcp-ts-core\u002Ftesting` and cast `ctx.log` to access the `.calls` array (the cast is necessary because `createMockContext` returns `Context`, which types `log` as `ContextLogger`):\n\n```ts\nimport { createMockContext, type MockContextLogger } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\n\nconst ctx = createMockContext();\nconst log = ctx.log as MockContextLogger;\n\nawait myTool.handler(input, ctx);\nexpect(log.calls.some(c => c.level === 'info' && c.msg.includes('Processing'))).toBe(true);\n```\n\n---\n\n## Full test example\n\n```ts\n\u002F\u002F tests\u002Ftools\u002Fmy-tool.tool.test.ts\nimport { describe, expect, it } from 'vitest';\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\nimport { myTool } from '@\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002Fmy-tool.tool.js';\n\ndescribe('myTool', () => {\n  it('returns expected output', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({ query: 'hello' });\n    const result = await myTool.handler(input, ctx);\n    expect(result.result).toBe('Found: hello');\n  });\n\n  it('throws on invalid state', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({ query: 'TRIGGER_ERROR' });\n    await expect(myTool.handler(input, ctx)).rejects.toThrow();\n  });\n\n  it('formats response completely', () => {\n    const result = { result: 'test' };\n    const blocks = myTool.format!(result);\n    expect(blocks[0].type).toBe('text');\n    expect((blocks[0] as { text?: string }).text).toContain('test');\n  });\n});\n```\n\nParse input through `myTool.input.parse(...)` to validate against the Zod schema and produce the typed input the handler expects. Call `myTool.handler(input, ctx)` directly, not through the MCP SDK or any framework wrapper. Assert on the return value for happy paths; use `.rejects.toThrow()` for error paths. Test `format` separately if the tool defines one — it's a pure function and needs no `ctx`. Verify the rendered text includes the fields the LLM needs, and for projection-style tools, add a case with non-default field selections.\n\n---\n\n## Testing with form-based client payloads\n\nLLM clients only send populated fields. **Form-based clients** (MCP Inspector, web UIs) submit the full schema shape — optional object fields arrive with empty-string inner values instead of `undefined`. Both are valid MCP usage. Test that handlers handle both gracefully.\n\n```ts\ndescribe('form-client payloads', () => {\n  it('skips optional object when inner fields are empty strings', async () => {\n    const ctx = createMockContext();\n    \u002F\u002F Form client sends the object with empty values instead of omitting it\n    const input = myTool.input.parse({\n      query: 'test',\n      dateRange: { minDate: '', maxDate: '' },\n    });\n    const result = await myTool.handler(input, ctx);\n    \u002F\u002F Should succeed — empty dateRange is ignored, not passed downstream\n    expect(result.items).toBeDefined();\n  });\n\n  it('uses optional object when inner fields have real values', async () => {\n    const ctx = createMockContext();\n    const input = myTool.input.parse({\n      query: 'test',\n      dateRange: { minDate: '2025-01-01', maxDate: '2025-12-31' },\n    });\n    const result = await myTool.handler(input, ctx);\n    \u002F\u002F Should apply the date filter\n    expect(result.items).toBeDefined();\n  });\n});\n```\n\nThe pattern: parse through the schema (confirms Zod accepts the payload), call the handler, assert the empty-value case produces correct results — no errors, no corrupted downstream queries. Same applies to optional arrays: test with `[]` to verify the handler skips rather than passes through.\n\n---\n\n## Testing with sparse upstream payloads\n\nThis is a different problem from form-client `''` payloads. Here the upstream API omits fields entirely. The risk is either a validation failure from an over-strict schema or a quiet lie where missing data turns into a concrete fact.\n\n```ts\ndescribe('sparse upstream payloads', () => {\n  it('preserves missing upstream fields as unknown', async () => {\n    const upstream = {\n      id: 'repo-123',\n      name: 'Widget Repo',\n      \u002F\u002F archived and star_count omitted entirely\n    };\n\n    const normalized = normalizeRepo(upstream);\n    expect(normalized).toEqual({\n      id: 'repo-123',\n      name: 'Widget Repo',\n    });\n\n    const output = repoSearchTool.output.parse({\n      repos: [normalized],\n    });\n    const blocks = repoSearchTool.format!(output);\n    expect((blocks[0] as { text: string }).text).toContain('Archived:** Not available');\n    expect((blocks[0] as { text: string }).text).not.toContain('Archived:** No');\n  });\n});\n```\n\n**What to verify:**\n\n- Fixtures omit fields entirely, not just set them to `null` or `''`.\n- Normalization\u002Fhelpers tolerate missing fields without fabricating defaults.\n- Handler output still validates against the declared output schema.\n- `format()` uses explicit unknown-state fallbacks instead of inventing facts.\n- Tool-semantic defaults are tested separately from upstream absence so the distinction stays clear.\n\n---\n\n## Vitest config\n\nExtend the framework's base config using `mergeConfig`. The base provides `globals: true`, `pool: 'forks'`, `isolate: true`, `tsconfigPaths`, and a Zod SSR compatibility fix. Add only the `@\u002F` alias for your server's source:\n\n```ts\n\u002F\u002F vitest.config.ts\nimport { defineConfig, mergeConfig } from 'vitest\u002Fconfig';\nimport coreConfig from '@cyanheads\u002Fmcp-ts-core\u002Fvitest.config';\n\nexport default mergeConfig(coreConfig, defineConfig({\n  resolve: {\n    alias: { '@\u002F': new URL('.\u002Fsrc\u002F', import.meta.url).pathname },\n  },\n}));\n```\n\n`mergeConfig` deep-merges the framework base with your overrides. The base sets `globals: true` (`describe`, `it`, `expect`, etc. available without imports), `pool: 'forks'` and `isolate: true` (test files run in separate worker processes), and `ssr: { noExternal: ['zod'] }` for Zod 4 compatibility. The `resolve.alias` entry maps `@\u002F` to `src\u002F`, matching the `paths` alias in `tsconfig.json` so imports like `@\u002Fservices\u002F...` resolve correctly in tests.\n\n---\n\n## Test isolation\n\n**Construct dependencies fresh in `beforeEach`.** Never share mutable state across tests.\n\n```ts\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { initMyService } from '@\u002Fservices\u002Fmy-domain\u002Fmy-service.js';\n\ndescribe('myTool with service', () => {\n  beforeEach(() => {\n    \u002F\u002F Re-initialize with a fresh instance before each test\n    initMyService(mockConfig, mockStorage);\n  });\n\n  it('calls service correctly', async () => {\n    const ctx = createMockContext({ tenantId: 'test-tenant' });\n    \u002F\u002F ...\n  });\n});\n```\n\n- Re-init services with `initMyService()` (or equivalent) in `beforeEach` when tests share a module-level singleton.\n- Vitest runs test files in separate workers — parallel file execution is safe by default.\n- Pass `createMockContext({ tenantId })` when a test needs a specific tenant; omitting it scopes state to `'default'`, not to a broken state surface.\n\n---\n\n## McpError assertions\n\n```ts\nimport { McpError, JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\n\nit('throws NotFound for missing resource', async () => {\n  const ctx = createMockContext();\n  const input = myTool.input.parse({ id: 'nonexistent' });\n  await expect(myTool.handler(input, ctx)).rejects.toMatchObject({\n    code: JsonRpcErrorCode.NotFound,\n  });\n});\n```\n\nUse `.rejects.toThrow(McpError)` to assert type only. Use `.rejects.toMatchObject({ code: ... })` when the specific error code matters.\n\n---\n\n## Output schema assertions\n\n`expect.schemaMatching` (Vitest 4, Standard Schema) validates a value against any Zod schema — including the definition's own `output`. Use it to assert schema conformance without duplicating the shape in the test:\n\n```ts\nit('output conforms to the declared output schema', async () => {\n  const ctx = createMockContext();\n  const result = await myTool.handler(myTool.input.parse({ query: 'x' }), ctx);\n  expect(result).toEqual(expect.schemaMatching(myTool.output));\n});\n```\n\nIt composes as an asymmetric matcher anywhere a value is expected — e.g. `toHaveBeenCalledWith(expect.schemaMatching(schema))`. Prefer exact-value assertions when the expected output is fully known; reach for `schemaMatching` when the output is dynamic (timestamps, generated IDs) or the schema itself is the contract under test.\n\n---\n\n## Testing handlers with `errors[]` (typed contract)\n\nTools and resources that declare an `errors[]` contract receive a typed `ctx.fail` helper at runtime. Pass the definition's own `errors` to `createMockContext` and the mock wires `fail` the same way the production handler factory does:\n\n```ts\nimport { createMockContext } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting';\nimport { JsonRpcErrorCode } from '@cyanheads\u002Fmcp-ts-core\u002Ferrors';\nimport { fetchItems } from '@\u002Fmcp-server\u002Ftools\u002Fdefinitions\u002Ffetch-items.tool.js';\n\nit('throws ctx.fail(\"no_match\") when no items resolve', async () => {\n  const ctx = createMockContext({ errors: fetchItems.errors });\n\n  const input = fetchItems.input.parse({ ids: ['missing'] });\n  await expect(fetchItems.handler(input, ctx)).rejects.toMatchObject({\n    code: JsonRpcErrorCode.NotFound,\n    data: { reason: 'no_match' },\n  });\n});\n```\n\nFor lower-level tests that need the raw `fail` helper without a full mock context (e.g. asserting the reason → code mapping), use `createFail` directly — see [Testing the handler-side `fail` plumbing](#testing-the-handler-side-fail-plumbing) below.\n\n### Why test `data.reason` and not just `code`?\n\nThe contract reason is the stable machine-readable identifier — clients switch on it the same way they would on an HTTP status. A code alone (`NotFound`) doesn't disambiguate between contract entries that share a code (`'no_match'` vs `'withdrawn'` both mapping to `NotFound`). Asserting on `data.reason` locks the test to the specific contract entry.\n\n### `data.reason` is overridable-proof\n\nThe framework spreads caller-supplied data first and writes `reason` last, so a handler that passes `data: { reason: 'something_else' }` cannot override the contract reason. Tests can rely on `data.reason` always equaling the contract entry's reason — write assertions that depend on it without paranoia.\n\n### Testing the handler-side `fail` plumbing\n\nTo verify the definition wires `ctx.fail` correctly without exercising the full handler factory, use the `errors` array directly:\n\n```ts\nimport { createFail } from '@cyanheads\u002Fmcp-ts-core';\n\nit('builds an error with the contract code and reason', () => {\n  const fail = createFail(myTool.errors!);\n  const err = fail('no_match', 'not found', { itemId: '123' });\n  expect(err.code).toBe(JsonRpcErrorCode.NotFound);\n  expect(err.data).toEqual({ reason: 'no_match', itemId: '123' });\n});\n```\n\n---\n\n## Fuzz testing\n\nFor schema-heavy or input-validation-critical handlers, the framework ships fuzz helpers under `@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Ffuzz`. They generate valid + adversarial inputs from your Zod schemas via `fast-check` and assert handler invariants (no crashes, no prototype pollution, no stack-trace leaks).\n\n```ts\nimport { fuzzTool, fuzzResource, fuzzPrompt } from '@cyanheads\u002Fmcp-ts-core\u002Ftesting\u002Ffuzz';\n\nit('survives fuzz testing', async () => {\n  const report = await fuzzTool(myTool, { numRuns: 100, numAdversarial: 30 });\n  expect(report.crashes).toHaveLength(0);\n  expect(report.leaks).toHaveLength(0);\n  expect(report.prototypePollution).toBe(false);\n});\n```\n\n| Helper | Purpose |\n|:-------|:--------|\n| `fuzzTool(def, opts)` \u002F `fuzzResource(def, opts)` \u002F `fuzzPrompt(def, opts)` | Drive valid + adversarial inputs through the handler. Returns a `FuzzReport`. |\n| `zodToArbitrary(schema)` | Convert a Zod schema to a `fast-check` `Arbitrary` for custom property-based tests. |\n| `adversarialArbitrary()` \u002F `ADVERSARIAL_STRINGS` | Targeted injection sets (prototype pollution probes, control characters, oversized payloads). |\n\n`FuzzOptions`: `numRuns` (default 50), `numAdversarial` (default 30), `seed` (reproducibility), `timeout` (per-call ms, default 5000), `ctx` (`MockContextOptions` for stateful handlers).\n\n`report.leaks` looks for a stack frame or a server-side path in what a client can observe — the `code`, `message`, and `data` of the thrown `McpError`. Strings the input itself supplied are removed before that check, so naming the offending value in error data (`throw validationError(msg, { key })`) never registers as a leak: the client sent those bytes and learns nothing from seeing them again.\n",{"name":5,"metadata":63,"description":67},{"type":48,"author":64,"version":65,"audience":66},"cyanheads","1.15","external","Testing patterns for MCP tool\u002Fresource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.\n","main","3df1d0d67b30ff15236561f8c0f9f06d33f42dfd","skills\u002Fmcp-servers\u002Fcyanheads-api-testing",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-13 02:31:19",{"source":8,"skillName":5,"commands":78,"prompt":88,"zipAvailable":89},[79,82,85],{"tool":80,"command":81},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill api-testing",{"tool":83,"command":84},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill api-testing",{"tool":86,"command":87},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill api-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-api-testing\u002FSKILL.md",true,[91,95,101],{"path":92,"resourceType":92,"resourceTypeDesc":93,"sizeBytes":94,"sourceHtmlUrl":16},"LICENSE","许可证",11317,{"path":96,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":99,"sourceHtmlUrl":100},"SOURCE.md","REFERENCE","参考文档",370,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-api-testing\u002FSOURCE.md",{"path":102,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":103,"sourceHtmlUrl":104},"TRUST.auto.yaml",2217,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fmcp-servers\u002Fcyanheads-api-testing\u002FTRUST.auto.yaml",[106,135,155,173,186,196],{"skillNo":107,"name":108,"nameZh":6,"description":109,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":110,"licenseFileUrl":111,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":58},"01M712C436EQYE43342Q4E7GG1","recipe-performance","Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134],"performance","recipe performance","recipe","mm","sentry","harness","app","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":136,"name":137,"nameZh":6,"description":138,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":139,"sourceHtmlUrl":140,"categories":141,"keywords":142,"publishedAt":58},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[143,144,145,37,146,147,148,149,150,151,42,152,153,154],"mexc spot websocket","spot","websocket","workflows","public","listen","streams","depth","keys","api keys","maintenance","protobuf",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":159,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":58},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[163,144,164,165,166,167,168,152,42,151,169,170,171,147,172,146,37],"mexc spot rest","REST","admin","signing","Wallet","requests","actions","secure","data","orders",{"skillNo":174,"name":175,"nameZh":6,"description":176,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":177,"sourceHtmlUrl":178,"categories":179,"keywords":180,"publishedAt":58},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[181,182,145,183,184,153,42,185,150,149,147,146],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":187,"name":188,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":110,"licenseFileUrl":189,"sourceHtmlUrl":190,"categories":191,"keywords":192,"publishedAt":58},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[193,194,195],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":197,"name":198,"nameZh":6,"description":199,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":110,"licenseFileUrl":200,"sourceHtmlUrl":201,"categories":202,"keywords":203,"publishedAt":58},"01M6H0Z28Q3R8J0S799GJQWXM3","verify-tests-cover-changed-lines","Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[204,205,206,207,208,209,210,211,212,213,214],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","use","patch","code","share"]