@yolk-sdk/emulators 0.1.0-canary.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- package/src/xai.ts +341 -0
package/src/mcp/state.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP emulator state, seed, profiles, and minted value forms (internal).
|
|
3
|
+
*
|
|
4
|
+
* The state holds the modern listing the seed selects and the legacy sessions minted since the
|
|
5
|
+
* last reset or seed; a seed holds no session, so no seeded value can take the minted
|
|
6
|
+
* `yolk-emu-session-<n>` form. The session counter and the cursor generation are runtime data
|
|
7
|
+
* that never reset (`McpRuntime`).
|
|
8
|
+
*
|
|
9
|
+
* @experimental
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto'
|
|
12
|
+
import { Result } from 'effect'
|
|
13
|
+
import * as Schema from 'effect/Schema'
|
|
14
|
+
|
|
15
|
+
/** The reserved origin both synthetic profiles answer on; another origin is not emulated. */
|
|
16
|
+
export const mcpEmulatorOrigin = 'https://mcp.example.test'
|
|
17
|
+
|
|
18
|
+
/** The synthetic modern server: stateless `2026-07-28`, JSON answers. */
|
|
19
|
+
export const mcpEmulatorModernPath = '/modern/mcp'
|
|
20
|
+
|
|
21
|
+
/** The synthetic legacy server: an `initialize` handshake, a session, and SSE answers. */
|
|
22
|
+
export const mcpEmulatorLegacyPath = '/legacy/mcp'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The public reserved invalid credential of the MCP auth case: the only credential value the
|
|
26
|
+
* emulator compares a bearer with (through its digest), answered the recorded 401.
|
|
27
|
+
*/
|
|
28
|
+
export const mcpEmulatorReservedInvalidCredential = 'yolk-conformance-invalid-credential-0000'
|
|
29
|
+
|
|
30
|
+
/** The minted session id form (`yolk-emu-session-<n>`, `n` from a counter that never resets). */
|
|
31
|
+
export const mcpEmulatorSessionPrefix = 'yolk-emu-session-'
|
|
32
|
+
|
|
33
|
+
/** At most this many sessions are held; an `initialize` beyond it is refused before any fault. */
|
|
34
|
+
export const mcpEmulatorSessionCap = 256
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Which recorded modern listing `tools/list` answers: `one-page` (the listing every calling
|
|
38
|
+
* fixture records; the default) or `two-pages` (the paged listing of `mcp.tools.list`, whose first
|
|
39
|
+
* page issues a cursor). Both come from fixtures; neither is synthesised.
|
|
40
|
+
*/
|
|
41
|
+
export const McpEmulatorModernListing = Schema.Literals(['one-page', 'two-pages'])
|
|
42
|
+
|
|
43
|
+
export type McpEmulatorModernListing = typeof McpEmulatorModernListing.Type
|
|
44
|
+
|
|
45
|
+
/** A legacy session: minted by `initialize`, ready after `notifications/initialized`. */
|
|
46
|
+
export const McpEmulatorSession = Schema.Struct({
|
|
47
|
+
id: Schema.String,
|
|
48
|
+
phase: Schema.Literals(['initializing', 'ready'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export type McpEmulatorSession = typeof McpEmulatorSession.Type
|
|
52
|
+
|
|
53
|
+
/** The emulator state (a JSON value the core snapshots and restores). */
|
|
54
|
+
export const McpEmulatorStateSchema = Schema.Struct({
|
|
55
|
+
modernListing: McpEmulatorModernListing,
|
|
56
|
+
sessions: Schema.Array(McpEmulatorSession)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export type McpEmulatorState = {
|
|
60
|
+
modernListing: McpEmulatorModernListing
|
|
61
|
+
sessions: ReadonlyArray<McpEmulatorSession>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The typed seed: only the modern listing; it holds no session. */
|
|
65
|
+
export const McpEmulatorSeed = Schema.Struct({
|
|
66
|
+
modernListing: Schema.optionalKey(McpEmulatorModernListing)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export type McpEmulatorSeed = typeof McpEmulatorSeed.Type
|
|
70
|
+
|
|
71
|
+
const strict = { onExcessProperty: 'error' } as const
|
|
72
|
+
|
|
73
|
+
const decodeSeed = Schema.decodeUnknownResult(McpEmulatorSeed, strict)
|
|
74
|
+
|
|
75
|
+
const decodeStateSchema = Schema.decodeUnknownResult(McpEmulatorStateSchema, strict)
|
|
76
|
+
|
|
77
|
+
const issueMessage = (issue: Schema.SchemaError['issue']): string =>
|
|
78
|
+
new Schema.SchemaError(issue).message
|
|
79
|
+
|
|
80
|
+
/** Build the state of a seed (no sessions), or why the seed is invalid. */
|
|
81
|
+
export const buildSeedState = (input: unknown): McpEmulatorState | string => {
|
|
82
|
+
const decoded = decodeSeed(input)
|
|
83
|
+
|
|
84
|
+
if (Result.isFailure(decoded)) return issueMessage(decoded.failure.issue)
|
|
85
|
+
|
|
86
|
+
return { modernListing: decoded.success.modernListing ?? 'one-page', sessions: [] }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Decode a whole state (the core's seed validation), or why it is invalid. */
|
|
90
|
+
export const decodeState = (input: unknown): McpEmulatorState | string => {
|
|
91
|
+
const decoded = decodeStateSchema(input)
|
|
92
|
+
|
|
93
|
+
return Result.isFailure(decoded)
|
|
94
|
+
? issueMessage(decoded.failure.issue)
|
|
95
|
+
: { modernListing: decoded.success.modernListing, sessions: decoded.success.sessions }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The one-way digest routes see instead of the bearer: SHA-256 of the arrival origin, a space,
|
|
100
|
+
* and the bearer (the wrapper's opt-in `bearerDigest`).
|
|
101
|
+
*/
|
|
102
|
+
export const mcpBearerDigest = (bearer: string, origin: string): string =>
|
|
103
|
+
createHash('sha256').update(`${origin} ${bearer}`, 'utf8').digest('hex')
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Runtime data (never in the state): the session counter and the cursor generation never reset;
|
|
107
|
+
* the cursor issued in the current generation is cleared on every reset and seed.
|
|
108
|
+
*/
|
|
109
|
+
export type McpRuntime = {
|
|
110
|
+
/** The number of the next minted session (never reset). */
|
|
111
|
+
nextSession: number
|
|
112
|
+
/** Starts at 1; every reset and seed starts the next generation (never reset). */
|
|
113
|
+
generation: number
|
|
114
|
+
/** The generation that first issued the recorded cursor (never reset). */
|
|
115
|
+
cursorFirstGeneration: number | undefined
|
|
116
|
+
/** The cursor issued in the current generation, if any. */
|
|
117
|
+
issuedCursor: string | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const makeMcpRuntime = (): McpRuntime => ({
|
|
121
|
+
nextSession: 1,
|
|
122
|
+
generation: 1,
|
|
123
|
+
cursorFirstGeneration: undefined,
|
|
124
|
+
issuedCursor: undefined
|
|
125
|
+
})
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful MCP emulator (the two synthetic servers of the MCP conformance fixtures: profile
|
|
3
|
+
* `synthetic-modern` on `https://mcp.example.test/modern/mcp`, stateless `2026-07-28` with JSON
|
|
4
|
+
* answers, and profile `synthetic-legacy` on `https://mcp.example.test/legacy/mcp`, an
|
|
5
|
+
* `initialize` handshake with sessions and SSE answers), built on the upstream `@emulators/core`
|
|
6
|
+
* custom runtime, with a request ledger, status and truncation faults, and an `/_emulate/*`
|
|
7
|
+
* control plane.
|
|
8
|
+
*
|
|
9
|
+
* It never imports SDK code: its answers are the sixteen synthetic `@yolk-sdk/mcp/conformance`
|
|
10
|
+
* fixtures copied as data (`src/mcp/recordings.ts`), and every manifest row names the conformance
|
|
11
|
+
* cases whose fixtures it answers (`mcpEmulatorRoutes`: one `RPC <origin><path>#<method>` row per
|
|
12
|
+
* recorded JSON-RPC method of each profile, plus the legacy `GET` row; no row writes anything in a
|
|
13
|
+
* real service). Response behaviour comes only from the fixtures, byte for byte: a JSON-RPC POST is
|
|
14
|
+
* answered only when it equals a recorded request within the latitude below, with the recorded
|
|
15
|
+
* answer, the request id substituted at exactly the recorded place (the top-level `id` of a JSON
|
|
16
|
+
* answer, or the `id` of the SSE response event's payload; notification events and SSE `id:` lines
|
|
17
|
+
* stay byte for byte; an answer that does not carry the recorded request id, such as the legacy era
|
|
18
|
+
* probe's `id: null` error, is unchanged). The legacy `initialize` mints a session
|
|
19
|
+
* `yolk-emu-session-<n>` (`n` from a counter that never resets, a form no seed holds), answered in
|
|
20
|
+
* the recorded `mcp-session-id` header; `notifications/initialized` on that session answers the
|
|
21
|
+
* recorded 202 and makes it ready; on a ready session `tools/list`, `tools/call`, and the standing
|
|
22
|
+
* `GET` (the recorded 405) answer the recordings with the session's id. At most 256 sessions are
|
|
23
|
+
* held: another `initialize` is refused before any fault. `reset` and `seed` clear the sessions.
|
|
24
|
+
* The seed only selects which recorded modern listing `tools/list` answers (`modernListing`:
|
|
25
|
+
* `one-page`, the default, or `two-pages`, whose first page issues the recorded cursor in the
|
|
26
|
+
* generation that first issues it, and `<cursor>.g<generation>` after a reset or seed; the second
|
|
27
|
+
* page answers only the cursor issued in the current generation). Every plan prepares its answer
|
|
28
|
+
* before anything is written; its commit (minting a session, readying one, issuing a cursor) runs
|
|
29
|
+
* only when no fault answers the request. Status faults (400-599) and `truncate-after-chunks`
|
|
30
|
+
* faults apply only after a request is admitted and planned, and a faulted request writes nothing:
|
|
31
|
+
* a truncation sends the prepared answer cut short and never runs the commit, so a truncated
|
|
32
|
+
* `initialize` holds no session and leaves the counter and the cap where they were, and a truncated
|
|
33
|
+
* first page issues no cursor. `match.route` selects one manifest row (a value naming no row is
|
|
34
|
+
* rejected when the fault is added); `match.method` is the HTTP method. Anything else answers one
|
|
35
|
+
* 400 not-emulated, ledgered with constant text only (`/<unrecognised>`, a standard method or
|
|
36
|
+
* `<other>`, an empty query, no headers or body, a constant reason, and the route template or row),
|
|
37
|
+
* that writes nothing and uses up no fault.
|
|
38
|
+
*
|
|
39
|
+
* Not emulated: `DELETE` (the client never sends it), `ping`, `resources/*`, `prompts/*`,
|
|
40
|
+
* `logging/*`, `completion/*`, `tasks/*`, JSON-RPC batches and client-sent responses, a JSON body
|
|
41
|
+
* repeating a key (the wrapper's opt-in `uniqueJsonKeys`), `mcp-*` headers no recording carries
|
|
42
|
+
* (such as `mcp-param-*`), cursors this emulator did not issue, any other tool or arguments, any
|
|
43
|
+
* other origin or path, and a missing `Authorization` (no fixture records the answer to one).
|
|
44
|
+
*
|
|
45
|
+
* Fail closed, on the shared wrapper's fail-closed mode as `/github`, `/google`, and
|
|
46
|
+
* `/linkedin-search` use it, with its opt-in `constantRefusals`, `guardAllHeaders`, and
|
|
47
|
+
* `guardOutput`: a request is recognised only on an emulated route shape, and any `Authorization`
|
|
48
|
+
* header is exactly `Bearer <token>` with a recognisable bearer (the RFC 6750 `b64token` syntax, at
|
|
49
|
+
* least 8 characters, a first character in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, and a
|
|
50
|
+
* character outside `[0-9.eE+-]`). A recognised request that repeats the bearer anywhere (its raw
|
|
51
|
+
* path, the raw query or any query key or value, any request header name or value other than
|
|
52
|
+
* `Authorization`, or its body, through the wrapper's fixpoint closure of tolerant percent-decoding
|
|
53
|
+
* and JSON-unescaping, capped, where a cap refuses) is ledgered with constant text only. The
|
|
54
|
+
* prepared output is checked the same way before any fault is decided or anything is committed: an
|
|
55
|
+
* answer (every header and chunk) or a minted session id or cursor that would repeat the bearer is
|
|
56
|
+
* refused with the constant credential-repeat entry, no fault used and nothing written, so a bearer
|
|
57
|
+
* such as `yolk-emu-session-1`, or `synthetic-mcp` (inside the recorded `yolk-synthetic-mcp`),
|
|
58
|
+
* never reaches a response, the state, or `/_emulate/*`. The bearer is never stored, forwarded,
|
|
59
|
+
* ledgered, or echoed: routes see only its digest (the wrapper's opt-in `bearerDigest`; SHA-256 of
|
|
60
|
+
* the origin, a space, and the bearer), which they compare only with the digest of the public
|
|
61
|
+
* reserved invalid credential `yolk-conformance-invalid-credential-0000` (itself a recognisable
|
|
62
|
+
* bearer) to answer its recorded 401 byte for byte. Scope: the bearer is never copied from the
|
|
63
|
+
* request into a response, the state, or `/_emulate/*`; the output guard also refuses a prepared
|
|
64
|
+
* fixture answer, minted session id, or cursor that happens to contain it, but the emulator's other
|
|
65
|
+
* constants (state values such as `initializing`, wrapper headers such as `x-emulator-evidence`)
|
|
66
|
+
* and host-configured control-plane data (a fault body) may coincidentally equal a bearer and are
|
|
67
|
+
* not checked. `makeMcpEmulator` also throws when a copied recording is not canonical JSON (every
|
|
68
|
+
* JSON body and SSE `data:` payload equal to `JSON.stringify(JSON.parse(text))`), which id
|
|
69
|
+
* substitution relies on.
|
|
70
|
+
*
|
|
71
|
+
* Request-shape latitude (`/mcp`, the only accepted deviations): any bearer value in the RFC 6750
|
|
72
|
+
* `b64token` syntax (`[A-Za-z0-9\-._~+/]+=*`) of at least 8 characters, starting with a character
|
|
73
|
+
* in `[G-Zg-z\-._~+/]` other than `n`, `r`, `t`, `u`, with at least one outside `[0-9.eE+-]`, that
|
|
74
|
+
* occurs nowhere else in the request (any header name or value included) and in no answer or value
|
|
75
|
+
* the request would store (never stored or ledgered; only its digest is compared, with the digest
|
|
76
|
+
* of the public reserved invalid credential `yolk-conformance-invalid-credential-0000`, which
|
|
77
|
+
* answers the recorded 401 on the era probe); extra request headers, except `mcp-*` headers other
|
|
78
|
+
* than `mcp-method`, `mcp-name`, `mcp-protocol-version`, and `mcp-session-id`; a recorded header
|
|
79
|
+
* value sent as several headers that the HTTP layer joins into the recorded value; JSON key order;
|
|
80
|
+
* any JSON-RPC request id that is an integer from 0 to 2^53 - 1 or 1 to 64 printable ASCII
|
|
81
|
+
* characters where the recording has an id; any non-empty `name` and `version` (and no other key)
|
|
82
|
+
* in the `_meta` client info (`io.modelcontextprotocol/clientInfo`) of a modern request; a session
|
|
83
|
+
* id this emulator minted since the last reset or seed where the recording sends `mcp-session-id`
|
|
84
|
+
* (initializing for `notifications/initialized`, ready otherwise); and, with the seed's `two-pages`
|
|
85
|
+
* listing, the cursor this emulator issued in the current generation on the second page.
|
|
86
|
+
* `Authorization` must be exactly `Bearer <token>` (that spelling, one space). Everything else
|
|
87
|
+
* (another origin or path, any query, other HTTP methods such as `DELETE` or a `GET` on the modern
|
|
88
|
+
* profile, JSON-RPC methods no fixture of the profile records such as `ping`, `resources/*`, or
|
|
89
|
+
* `prompts/*`, batches and client-sent responses, other members, a JSON body repeating a key
|
|
90
|
+
* (compared after unescaping), a `null`, negative, or fractional id, other params (other tools,
|
|
91
|
+
* arguments, protocol versions, or capabilities, extra client-info keys, and a legacy `initialize`
|
|
92
|
+
* client info other than the recorded one), the MCP headers `accept`, `content-type`, `mcp-method`,
|
|
93
|
+
* `mcp-protocol-version`, `mcp-name`, and `last-event-id` other than the recorded values or present
|
|
94
|
+
* where none is recorded, any other `mcp-*` header (such as `mcp-param-*`), `mcp-session-id`
|
|
95
|
+
* missing where recorded or present where not, an unknown session or one in the wrong phase, a
|
|
96
|
+
* cursor not issued in the current generation, the reserved invalid credential on anything but the
|
|
97
|
+
* era probe, a bearer repeated anywhere in the request, and a bearer an answer or a minted session
|
|
98
|
+
* id or cursor would repeat) is not emulated.
|
|
99
|
+
*
|
|
100
|
+
* Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
|
|
101
|
+
* `makeMcpEmulator` (importing this module has no side effects).
|
|
102
|
+
*
|
|
103
|
+
* @experimental
|
|
104
|
+
*/
|
|
105
|
+
import { Data, Predicate } from 'effect'
|
|
106
|
+
import {
|
|
107
|
+
mcpApiRoutes,
|
|
108
|
+
mcpEmulatorDrillKnobs,
|
|
109
|
+
mcpRecordingProblems,
|
|
110
|
+
type McpApiEnv,
|
|
111
|
+
type McpEmulatorDrills
|
|
112
|
+
} from './mcp/api.ts'
|
|
113
|
+
import { mcpEmulatorFixtures } from './mcp/recordings.ts'
|
|
114
|
+
import {
|
|
115
|
+
buildSeedState,
|
|
116
|
+
decodeState,
|
|
117
|
+
makeMcpRuntime,
|
|
118
|
+
mcpBearerDigest,
|
|
119
|
+
mcpEmulatorOrigin,
|
|
120
|
+
mcpEmulatorReservedInvalidCredential,
|
|
121
|
+
type McpEmulatorSeed,
|
|
122
|
+
type McpEmulatorState
|
|
123
|
+
} from './mcp/state.ts'
|
|
124
|
+
import type { EmulatorRouteEvidence } from './route-evidence.ts'
|
|
125
|
+
import {
|
|
126
|
+
StatefulFaultMatch,
|
|
127
|
+
StatefulStreamFault,
|
|
128
|
+
checkBooleanDrills,
|
|
129
|
+
makeChunkedStatefulEmulator,
|
|
130
|
+
routeManifest,
|
|
131
|
+
type StatefulCoverage,
|
|
132
|
+
type StatefulEmulatorApi,
|
|
133
|
+
type StatefulFaultState,
|
|
134
|
+
type StatefulInputKind,
|
|
135
|
+
type StatefulLedgerEntry
|
|
136
|
+
} from './stateful-emulator.ts'
|
|
137
|
+
import { statefulCoreRuntime } from './stateful-core.ts'
|
|
138
|
+
import { isRecognisableBearerValue } from './stateful-secrets.ts'
|
|
139
|
+
|
|
140
|
+
export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
|
|
141
|
+
|
|
142
|
+
export { emulatorEvidenceHeader } from './route-evidence.ts'
|
|
143
|
+
|
|
144
|
+
export { isMcpEmulatedRequestId, mcpEmulatorDrillKnobs, type McpEmulatorDrills } from './mcp/api.ts'
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
McpEmulatorModernListing,
|
|
148
|
+
McpEmulatorSeed,
|
|
149
|
+
McpEmulatorSession,
|
|
150
|
+
McpEmulatorStateSchema,
|
|
151
|
+
mcpEmulatorLegacyPath,
|
|
152
|
+
mcpEmulatorModernPath,
|
|
153
|
+
mcpEmulatorOrigin,
|
|
154
|
+
mcpEmulatorReservedInvalidCredential,
|
|
155
|
+
mcpEmulatorSessionCap,
|
|
156
|
+
mcpEmulatorSessionPrefix,
|
|
157
|
+
type McpEmulatorState
|
|
158
|
+
} from './mcp/state.ts'
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
mcpEmulatorFixtures,
|
|
162
|
+
type McpRecordedExchange,
|
|
163
|
+
type McpRecordedFixture,
|
|
164
|
+
type McpRecordedRequest,
|
|
165
|
+
type McpRecordedResponse
|
|
166
|
+
} from './mcp/recordings.ts'
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Route evidence manifest: one `RPC <origin><path>#<method>` row per recorded JSON-RPC method of
|
|
170
|
+
* each profile, plus the legacy `GET` row, with the conformance cases whose (synthetic,
|
|
171
|
+
* unverified) fixtures each answers. No row writes anything. Kept in sync with the handlers by
|
|
172
|
+
* construction (both come from one route table).
|
|
173
|
+
*/
|
|
174
|
+
export const mcpEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =
|
|
175
|
+
mcpApiRoutes.flatMap(routeManifest)
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Optional fault filter; an omitted field matches every request. `method` is the HTTP method
|
|
179
|
+
* (`POST` or `GET`, never a row's `RPC`); `path` is the raw request path (ending in `*`, a
|
|
180
|
+
* prefix); `route` is a manifest row path (for example
|
|
181
|
+
* `https://mcp.example.test/legacy/mcp#tools/list`), and a value naming no row is rejected when
|
|
182
|
+
* the fault is added.
|
|
183
|
+
*/
|
|
184
|
+
export const McpFaultMatch = StatefulFaultMatch
|
|
185
|
+
|
|
186
|
+
export type McpFaultMatch = typeof StatefulFaultMatch.Type
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A status fault (400-599), or a `truncate-after-chunks` fault that sends the first `chunks`
|
|
190
|
+
* chunks of the prepared answer and then ends it cleanly (an SSE answer has two chunks, a JSON
|
|
191
|
+
* answer one). Only a request the emulator would answer reaches a fault: a request that is not
|
|
192
|
+
* emulated, by its shape or by the state, never uses one up. A faulted request writes nothing (a
|
|
193
|
+
* truncated `initialize` mints no session; a truncated first page issues no cursor). A truncation
|
|
194
|
+
* that cannot apply answers 500 and is not used up.
|
|
195
|
+
*/
|
|
196
|
+
export const McpFault = StatefulStreamFault
|
|
197
|
+
|
|
198
|
+
export type McpFault = StatefulStreamFault
|
|
199
|
+
|
|
200
|
+
export type McpFaultState = StatefulFaultState<McpFault>
|
|
201
|
+
|
|
202
|
+
export type McpLedgerEntry = StatefulLedgerEntry
|
|
203
|
+
|
|
204
|
+
export type McpCoverage = StatefulCoverage
|
|
205
|
+
|
|
206
|
+
/** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
|
|
207
|
+
export class McpEmulatorInputInvalid extends Data.TaggedError('McpEmulatorInputInvalid')<{
|
|
208
|
+
readonly input: StatefulInputKind
|
|
209
|
+
readonly reason: string
|
|
210
|
+
}> {
|
|
211
|
+
override get message(): string {
|
|
212
|
+
return `Invalid MCP emulator ${this.input}: ${this.reason}`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type McpEmulatorOptions = {
|
|
217
|
+
/** Typed seed; defaults to the one-page modern listing (sessions are never seeded). */
|
|
218
|
+
readonly seed?: McpEmulatorSeed
|
|
219
|
+
/** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
|
|
220
|
+
readonly drills?: McpEmulatorDrills
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type McpEmulator = StatefulEmulatorApi<McpEmulatorState, McpEmulatorSeed, McpFault>
|
|
224
|
+
|
|
225
|
+
const inputInvalid = (input: StatefulInputKind, reason: string) =>
|
|
226
|
+
new McpEmulatorInputInvalid({ input, reason })
|
|
227
|
+
|
|
228
|
+
/** The constant reason of a request on no emulated route shape. */
|
|
229
|
+
const unrecognisedReason = 'no emulated MCP route for this method and path'
|
|
230
|
+
|
|
231
|
+
/** The constant reason of a request whose `Authorization` header is not one recognisable bearer. */
|
|
232
|
+
const unrecognisedAuthorizationReason = 'an unrecognisable Authorization header is not emulated'
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create a stateful MCP emulator on the `@emulators/core` custom runtime. Each call has its own
|
|
236
|
+
* state, ledger, faults, and session counter. Rejects with `McpEmulatorInputInvalid` for an
|
|
237
|
+
* invalid seed or option. See `src/stateful-emulator.ts` for the request precedence.
|
|
238
|
+
*/
|
|
239
|
+
export const makeMcpEmulator = async (options: McpEmulatorOptions = {}): Promise<McpEmulator> => {
|
|
240
|
+
const initial = buildSeedState(options.seed ?? {})
|
|
241
|
+
|
|
242
|
+
if (Predicate.isString(initial)) {
|
|
243
|
+
throw inputInvalid('seed', initial)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
checkBooleanDrills(options.drills, mcpEmulatorDrillKnobs, inputInvalid)
|
|
247
|
+
|
|
248
|
+
// The reserved invalid credential must reach the routes as a recognisable bearer's digest; a
|
|
249
|
+
// value the bearer rule refused would never be answered its recorded 401.
|
|
250
|
+
if (!isRecognisableBearerValue(mcpEmulatorReservedInvalidCredential)) {
|
|
251
|
+
throw new Error('the reserved invalid MCP credential is not a recognisable bearer')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Id substitution re-serialises the response message: a recording copied in a non-canonical
|
|
255
|
+
// JSON form would answer other bytes, so it fails here, loudly.
|
|
256
|
+
const recordingProblems = mcpRecordingProblems(mcpEmulatorFixtures)
|
|
257
|
+
|
|
258
|
+
if (recordingProblems.length > 0) {
|
|
259
|
+
throw new Error(`non-canonical MCP recordings: ${recordingProblems.join('; ')}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const drills = options.drills ?? {}
|
|
263
|
+
const runtime = makeMcpRuntime()
|
|
264
|
+
|
|
265
|
+
const env: McpApiEnv = {
|
|
266
|
+
drills: {
|
|
267
|
+
discoverCarriesErrorResponse: drills.discoverCarriesErrorResponse === true,
|
|
268
|
+
discoverWithoutResultType: drills.discoverWithoutResultType === true,
|
|
269
|
+
sessionIdNotVisibleAscii: drills.sessionIdNotVisibleAscii === true,
|
|
270
|
+
discoverAnsweredTwice: drills.discoverAnsweredTwice === true,
|
|
271
|
+
writeToolMarkedReadOnly: drills.writeToolMarkedReadOnly === true,
|
|
272
|
+
readCallAnswersToolError: drills.readCallAnswersToolError === true,
|
|
273
|
+
invalidCallAnswersRpcError: drills.invalidCallAnswersRpcError === true,
|
|
274
|
+
absentCallAnswersResult: drills.absentCallAnswersResult === true,
|
|
275
|
+
unauthorizedWithoutChallenge: drills.unauthorizedWithoutChallenge === true
|
|
276
|
+
},
|
|
277
|
+
reservedDigest: mcpBearerDigest(mcpEmulatorReservedInvalidCredential, mcpEmulatorOrigin),
|
|
278
|
+
runtime
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return makeChunkedStatefulEmulator<McpEmulatorState, McpApiEnv, McpEmulatorSeed>(
|
|
282
|
+
{
|
|
283
|
+
routes: mcpApiRoutes,
|
|
284
|
+
env,
|
|
285
|
+
initial,
|
|
286
|
+
buildSeed: buildSeedState,
|
|
287
|
+
recordHeaders: [
|
|
288
|
+
{ name: 'accept', json: false },
|
|
289
|
+
{ name: 'content-type', json: false },
|
|
290
|
+
{ name: 'mcp-method', json: false },
|
|
291
|
+
{ name: 'mcp-name', json: false },
|
|
292
|
+
{ name: 'mcp-protocol-version', json: false },
|
|
293
|
+
{ name: 'mcp-session-id', json: false }
|
|
294
|
+
],
|
|
295
|
+
failClosed: {
|
|
296
|
+
unrecognised: unrecognisedReason,
|
|
297
|
+
unrecognisedAuthorization: unrecognisedAuthorizationReason
|
|
298
|
+
},
|
|
299
|
+
bearerDigest: mcpBearerDigest,
|
|
300
|
+
constantRefusals: true,
|
|
301
|
+
guardAllHeaders: true,
|
|
302
|
+
guardOutput: true,
|
|
303
|
+
uniqueJsonKeys: true,
|
|
304
|
+
// Every reset and seed starts a cursor generation; the session counter never resets.
|
|
305
|
+
clearRuntime: () => {
|
|
306
|
+
runtime.generation += 1
|
|
307
|
+
runtime.issuedCursor = undefined
|
|
308
|
+
},
|
|
309
|
+
runtimeState: () => ({
|
|
310
|
+
nextSession: runtime.nextSession,
|
|
311
|
+
cursorGeneration: runtime.generation,
|
|
312
|
+
issuedCursor: runtime.issuedCursor ?? null
|
|
313
|
+
}),
|
|
314
|
+
seedSummary: state => ({ modernListing: state.modernListing }),
|
|
315
|
+
inputInvalid
|
|
316
|
+
},
|
|
317
|
+
statefulCoreRuntime({ name: 'mcp', initial, decodeState, inputInvalid })
|
|
318
|
+
)
|
|
319
|
+
}
|