@george43g/mcp-kit 0.1.0

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/dispatch.d.ts +58 -0
  4. package/dist/dispatch.d.ts.map +1 -0
  5. package/dist/dispatch.js +110 -0
  6. package/dist/dispatch.js.map +1 -0
  7. package/dist/index.d.ts +13 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +8 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/prompt-injection.d.ts +37 -0
  12. package/dist/prompt-injection.d.ts.map +1 -0
  13. package/dist/prompt-injection.js +46 -0
  14. package/dist/prompt-injection.js.map +1 -0
  15. package/dist/resources.d.ts +81 -0
  16. package/dist/resources.d.ts.map +1 -0
  17. package/dist/resources.js +66 -0
  18. package/dist/resources.js.map +1 -0
  19. package/dist/sanitize.d.ts +18 -0
  20. package/dist/sanitize.d.ts.map +1 -0
  21. package/dist/sanitize.js +33 -0
  22. package/dist/sanitize.js.map +1 -0
  23. package/dist/tool-registry.d.ts +78 -0
  24. package/dist/tool-registry.d.ts.map +1 -0
  25. package/dist/tool-registry.js +38 -0
  26. package/dist/tool-registry.js.map +1 -0
  27. package/dist/transports/http.d.ts +47 -0
  28. package/dist/transports/http.d.ts.map +1 -0
  29. package/dist/transports/http.js +150 -0
  30. package/dist/transports/http.js.map +1 -0
  31. package/dist/transports/stdio.d.ts +17 -0
  32. package/dist/transports/stdio.d.ts.map +1 -0
  33. package/dist/transports/stdio.js +53 -0
  34. package/dist/transports/stdio.js.map +1 -0
  35. package/package.json +72 -0
  36. package/src/dispatch.ts +172 -0
  37. package/src/index.ts +34 -0
  38. package/src/prompt-injection.ts +56 -0
  39. package/src/resources.ts +120 -0
  40. package/src/sanitize.ts +37 -0
  41. package/src/tool-registry.ts +111 -0
  42. package/src/transports/http.ts +188 -0
  43. package/src/transports/stdio.ts +70 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 George G
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @george43g/mcp-kit
2
+
3
+ MCP server building blocks: a tool registry, a dispatcher with timeout / perf /
4
+ abort / error-wrapping baked in, stdio + Streamable HTTP transports, `sanitize()`
5
+ for untrusted content, and UUID-gated prompt-injection helpers.
6
+
7
+ Not yet on npm. It is vendored into generated repos today; see `DEFERRED.md` #25
8
+ in the template repo for the publish sequence.
9
+
10
+ ## The dispatcher is the point
11
+
12
+ Every tool call goes through one function, and that function holds invariants
13
+ that are easy to lose when each tool wires itself:
14
+
15
+ 1. Every handler runs inside `withTimeout` — per-tool `timeoutMs`, else
16
+ `MCP_TOOL_TIMEOUT_DEFAULT_MS` (30s).
17
+ 2. `noteActivity()` fires on every dispatch, feeding the idle watchdog.
18
+ 3. A `perf()` span wraps every handler; its duration lands in `_meta`.
19
+ 4. Errors are wrapped with the tool name and an actionable hint — never a bare
20
+ `error.message`.
21
+ 5. `AbortSignal` is passed through.
22
+ 6. Nothing writes to stdout after `StdioServerTransport.connect()` — JSON-RPC
23
+ owns it. Log through `@george43g/robustness`.
24
+
25
+ ```ts
26
+ import { buildDispatcher, makeRegistry } from "@george43g/mcp-kit";
27
+
28
+ const registry = makeRegistry([healthCheck, noop]);
29
+ const dispatch = buildDispatcher({ registry, engineLabel: () => "ts" });
30
+ ```
31
+
32
+ ## Content blocks
33
+
34
+ A tool may emit media alongside its JSON summary. `toContent` derives the extra
35
+ blocks from the result, and the dispatcher emits them **ahead** of the text
36
+ block:
37
+
38
+ ```ts
39
+ const screenshot: ToolDefinition = {
40
+ // ...
41
+ handler: async () => ({ path: "/tmp/shot.png" }),
42
+ toContent: (r) => [{ type: "image", data: readBase64(r.path), mimeType: "image/png" }],
43
+ };
44
+
45
+ // dispatch("screenshot", {}) → content: [image, text]
46
+ ```
47
+
48
+ Three things worth knowing before you use it:
49
+
50
+ - **`[media, text]` is a contract, not an implementation detail.** `cli-kit`'s
51
+ renderer prints the image line above the payload, and consumers index on the
52
+ order.
53
+ - **`data` is RAW base64** — no `data:` URI prefix, and not a path.
54
+ - **A throwing `toContent` degrades to text.** The throw is caught, logged as
55
+ `to_content_failed`, and the tool still returns its answer. A tool that cannot
56
+ render its picture is not a tool that failed.
57
+
58
+ `ContentBlock` is deliberately the same two members as `@george43g/cli-kit`'s,
59
+ and deliberately has **no catch-all member**. A catch-all overlaps
60
+ `type: "text"`, so every narrowing site needs a cast — and a compile error is
61
+ *wanted* when a new block type appears: it lands exactly at the render site
62
+ where somebody has to decide how to draw it. Reading a block's text narrows:
63
+
64
+ ```ts
65
+ for (const block of result.content) {
66
+ if (block.type === "text") process.stdout.write(block.text);
67
+ }
68
+ ```
69
+
70
+ ## Dev-only tools: hiding is not disabling
71
+
72
+ `devOnly: true` was honoured only by `toMcpTools()`. The tool vanished from
73
+ `tools/list` and **still executed if you named it**, and every non-MCP caller —
74
+ a CLI, a REPL's tool list — bypassed the filter entirely.
75
+
76
+ ```ts
77
+ const dispatch = buildDispatcher({
78
+ registry,
79
+ devOnlyEnabled: () => process.env.MCP_DEV === "1",
80
+ });
81
+ ```
82
+
83
+ - Read **per dispatch**, not at construction, so flipping the env mid-suite
84
+ takes effect.
85
+ - A gated tool's response is **identical to an unknown tool name**. A distinct
86
+ "this tool is disabled" error confirms the tool exists, which is what the gate
87
+ is for.
88
+ - **Omit it and nothing changes** — dev-only tools stay callable, which is what
89
+ this dispatcher did before the option existed.
90
+
91
+ ## Transports
92
+
93
+ ```ts
94
+ import { startStdio } from "@george43g/mcp-kit/stdio";
95
+ import { startHttpServer } from "@george43g/mcp-kit/http";
96
+ ```
97
+
98
+ HTTP is single-tenant by design: one server process, one identity. The bearer
99
+ token is caller-supplied or read from `MCP_HTTP_TOKEN`.
100
+
101
+ ## Untrusted content
102
+
103
+ `sanitize()` strips control characters and bounds length. `wrapToolError()` and
104
+ the UUID-gated helpers in `prompt-injection.ts` fence tool output so a tool's
105
+ own error text cannot impersonate the host.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Dispatcher — the single place every MCP tool call passes through.
3
+ *
4
+ * DISPATCHER INVARIANTS (do not weaken without consulting AGENTS.md):
5
+ * 1. Every tool runs through withTimeout — declare a per-tool timeout
6
+ * in ToolDefinition.timeoutMs or rely on MCP_TOOL_TIMEOUT_DEFAULT_MS.
7
+ * 2. noteActivity() fires on every dispatch (feeds the idle watchdog).
8
+ * 3. perf() span around every handler.
9
+ * 4. Errors wrapped with actionable hint + tool name (never bare error.message).
10
+ * 5. AbortSignal honored — long-running handlers check signal?.aborted
11
+ * between iterations and return early with a structured error.
12
+ * 6. NEVER console.log after StdioServerTransport.connect() — JSON-RPC owns
13
+ * stdout. Log via @george43g/robustness/logger.
14
+ * 7. Tool responses return { content, structuredContent } per the MCP spec,
15
+ * plus a perf footer (engine + dur_ms) in `_meta`.
16
+ *
17
+ * The exported `buildDispatcher` returns a function whose shape matches the
18
+ * SDK's `CallToolRequest` handler.
19
+ */
20
+ import type { ContentBlock, ToolRegistry } from "./tool-registry.js";
21
+ export interface ToolResult {
22
+ content: ContentBlock[];
23
+ structuredContent?: unknown;
24
+ isError?: boolean;
25
+ _meta?: Record<string, unknown>;
26
+ }
27
+ export interface DispatcherCounters {
28
+ /** Total successful + failed tool calls. */
29
+ toolCalls: number;
30
+ /** Errors in the last 5 minutes (caller maintains via dispatcher hook). */
31
+ recentErrors: number;
32
+ }
33
+ export interface BuildDispatcherOptions {
34
+ registry: ToolRegistry;
35
+ /** Called with the ToolTimeoutError / handler-thrown error to bump counters. */
36
+ onError?: (toolName: string, err: unknown) => void;
37
+ /** Called per dispatch (success or failure) to bump call counters. */
38
+ onCall?: (toolName: string) => void;
39
+ /** Engine label, e.g. "rust" or "ts". Surfaced in `_meta` and dev stats. */
40
+ engineLabel?: () => string;
41
+ /**
42
+ * Whether `devOnly` tools are callable. Evaluated PER DISPATCH, not once at
43
+ * construction, so flipping the env in a test takes effect immediately.
44
+ *
45
+ * Omitted, dev-only tools stay callable — which is what this dispatcher did
46
+ * before the option existed, so adding it changes nothing until you pass it.
47
+ *
48
+ * The gap it closes: `devOnly` was honoured only by `toMcpTools()`, so the
49
+ * tool was hidden from `tools/list` and still executed if you named it
50
+ * anyway, and every non-MCP caller (a CLI, a REPL tool list) bypassed the
51
+ * filter entirely. **Hiding a tool is not disabling it.** Found and fixed in
52
+ * browser-tab-mcp's vendored copy of this file.
53
+ */
54
+ devOnlyEnabled?: () => boolean;
55
+ }
56
+ export type Dispatch = (name: string, args: unknown, signal?: AbortSignal) => Promise<ToolResult>;
57
+ export declare function buildDispatcher(opts: BuildDispatcherOptions): Dispatch;
58
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAaH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAErE,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,YAAY,CAAC;IACvB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAUlG,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,CA0FtE"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Dispatcher — the single place every MCP tool call passes through.
3
+ *
4
+ * DISPATCHER INVARIANTS (do not weaken without consulting AGENTS.md):
5
+ * 1. Every tool runs through withTimeout — declare a per-tool timeout
6
+ * in ToolDefinition.timeoutMs or rely on MCP_TOOL_TIMEOUT_DEFAULT_MS.
7
+ * 2. noteActivity() fires on every dispatch (feeds the idle watchdog).
8
+ * 3. perf() span around every handler.
9
+ * 4. Errors wrapped with actionable hint + tool name (never bare error.message).
10
+ * 5. AbortSignal honored — long-running handlers check signal?.aborted
11
+ * between iterations and return early with a structured error.
12
+ * 6. NEVER console.log after StdioServerTransport.connect() — JSON-RPC owns
13
+ * stdout. Log via @george43g/robustness/logger.
14
+ * 7. Tool responses return { content, structuredContent } per the MCP spec,
15
+ * plus a perf footer (engine + dur_ms) in `_meta`.
16
+ *
17
+ * The exported `buildDispatcher` returns a function whose shape matches the
18
+ * SDK's `CallToolRequest` handler.
19
+ */
20
+ import { envNum, error as logError, warn as logWarn, noteActivity, perf, ToolTimeoutError, withTimeout, } from "@george43g/robustness";
21
+ import { wrapToolError } from "./prompt-injection.js";
22
+ function defaultTimeoutMs() {
23
+ return envNum("MCP_TOOL_TIMEOUT_DEFAULT_MS", 30_000);
24
+ }
25
+ function formatZodError(err) {
26
+ return err.errors.map((e) => ` - ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
27
+ }
28
+ export function buildDispatcher(opts) {
29
+ const fallback = defaultTimeoutMs();
30
+ return async (name, args, signal) => {
31
+ noteActivity();
32
+ opts.onCall?.(name);
33
+ const def = opts.registry.get(name);
34
+ // A dev-only tool that is not enabled must be INDISTINGUISHABLE from one
35
+ // that does not exist. A distinct "disabled" error confirms the tool is
36
+ // there, which is the thing a gate exists to avoid.
37
+ const devGated = def?.devOnly === true && opts.devOnlyEnabled !== undefined ? !opts.devOnlyEnabled() : false;
38
+ if (!def || devGated) {
39
+ opts.onError?.(name, new Error("unknown_tool"));
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: wrapToolError(name, "Unknown tool name.", "Check the result of tools/list."),
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ const parsed = def.input.safeParse(args ?? {});
51
+ if (!parsed.success) {
52
+ opts.onError?.(name, parsed.error);
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: wrapToolError(name, "Invalid arguments:", `\n${formatZodError(parsed.error)}`),
58
+ },
59
+ ],
60
+ isError: true,
61
+ };
62
+ }
63
+ const span = perf(`dispatch.${name}`);
64
+ const timeoutMs = def.timeoutMs ?? fallback;
65
+ try {
66
+ const result = await withTimeout(name, () => def.handler(parsed.data, signal), timeoutMs);
67
+ const dur = span.end({ engine: opts.engineLabel?.() ?? "ts" });
68
+ const textBlock = {
69
+ type: "text",
70
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
71
+ };
72
+ // Media blocks lead, the JSON summary follows: a screenshot tool returns
73
+ // `[image, text]`. Renderers depend on that order — cli-kit's prints the
74
+ // image line above the payload — so it is a contract, not a detail.
75
+ let extra = [];
76
+ if (def.toContent) {
77
+ try {
78
+ extra = def.toContent(result);
79
+ }
80
+ catch (err) {
81
+ logWarn(`to_content_failed: ${name}`, { message: err?.message });
82
+ }
83
+ }
84
+ return {
85
+ content: [...extra, textBlock],
86
+ structuredContent: result,
87
+ _meta: {
88
+ engine: opts.engineLabel?.() ?? "ts",
89
+ duration_ms: Math.round(dur * 10) / 10,
90
+ },
91
+ };
92
+ }
93
+ catch (err) {
94
+ span.end({ error: true });
95
+ opts.onError?.(name, err);
96
+ logError(`dispatch_error: ${name}`, {
97
+ message: err?.message,
98
+ stack: err?.stack,
99
+ });
100
+ const message = err instanceof ToolTimeoutError
101
+ ? `Timed out after ${err.timeoutMs}ms. The server has unblocked; the underlying work may still be running.`
102
+ : (err?.message ?? String(err));
103
+ return {
104
+ content: [{ type: "text", text: wrapToolError(name, message) }],
105
+ isError: true,
106
+ };
107
+ }
108
+ };
109
+ }
110
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type { BuildDispatcherOptions, Dispatch, DispatcherCounters, ToolResult, } from "./dispatch.js";
2
+ export { buildDispatcher } from "./dispatch.js";
3
+ export { type WrapInstructionsResult, wrapInstructions, wrapToolError, wrapUntrusted, } from "./prompt-injection.js";
4
+ export type { BuildResourcesHandlerOptions, ResourceContent, ResourceHandlers, ResourceListEntry, ResourcesProvider, ResourceTemplate, } from "./resources.js";
5
+ export { buildResourcesHandler } from "./resources.js";
6
+ export { sanitize } from "./sanitize.js";
7
+ export type { AnyToolDefinition, ContentBlock, ToolDefinition, ToolRegistry, } from "./tool-registry.js";
8
+ export { makeRegistry } from "./tool-registry.js";
9
+ export type { HttpServerHandle, HttpServerOptions } from "./transports/http.js";
10
+ export { startHttpServer } from "./transports/http.js";
11
+ export type { StartStdioOptions } from "./transports/stdio.js";
12
+ export { startStdio } from "./transports/stdio.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,UAAU,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,4BAA4B,EAC5B,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { buildDispatcher } from "./dispatch.js";
2
+ export { wrapInstructions, wrapToolError, wrapUntrusted, } from "./prompt-injection.js";
3
+ export { buildResourcesHandler } from "./resources.js";
4
+ export { sanitize } from "./sanitize.js";
5
+ export { makeRegistry } from "./tool-registry.js";
6
+ export { startHttpServer } from "./transports/http.js";
7
+ export { startStdio } from "./transports/stdio.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAEL,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,uBAAuB,CAAC;AAS/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAOzC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Prompt-injection defense helpers.
3
+ *
4
+ * Two primitives:
5
+ *
6
+ * 1. `wrapUntrusted(text)` — wrap arbitrary content with `<untrusted>...
7
+ * </untrusted>` markers. Use whenever a tool returns content sourced
8
+ * from an external system (user message, scraped page, etc.). The LLM
9
+ * is trained to treat tagged content as data, not instructions.
10
+ *
11
+ * 2. `wrapInstructions(text, uuid)` — emit instructions to the LLM that
12
+ * only "fire" when the same UUID is present in the user's most recent
13
+ * message. Pattern documented in docs/GUARDRAILS_MCP_RESPONSES.md.
14
+ *
15
+ * Tool returns:
16
+ * <instructions uuid="d8a3..."> follow steps... </instructions>
17
+ *
18
+ * The LLM, having been trained on this pattern, will refuse to follow
19
+ * the embedded instructions unless the user's prior message contains
20
+ * the UUID — proving the user genuinely wanted to authorize them.
21
+ *
22
+ * Use sparingly: prefer not to embed instructions in tool responses
23
+ * at all. When you must (multi-step workflows, follow-ups), use this
24
+ * wrapper.
25
+ */
26
+ export declare function wrapUntrusted(text: string): string;
27
+ export interface WrapInstructionsResult {
28
+ text: string;
29
+ uuid: string;
30
+ }
31
+ export declare function wrapInstructions(text: string, uuid?: string): WrapInstructionsResult;
32
+ /**
33
+ * Wrap an actionable error message so the LLM gets a hint without
34
+ * treating the rest of the response as instructions.
35
+ */
36
+ export declare function wrapToolError(toolName: string, message: string, hint?: string): string;
37
+ //# sourceMappingURL=prompt-injection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-injection.d.ts","sourceRoot":"","sources":["../src/prompt-injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAqB,GAC1B,sBAAsB,CAKxB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAItF"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Prompt-injection defense helpers.
3
+ *
4
+ * Two primitives:
5
+ *
6
+ * 1. `wrapUntrusted(text)` — wrap arbitrary content with `<untrusted>...
7
+ * </untrusted>` markers. Use whenever a tool returns content sourced
8
+ * from an external system (user message, scraped page, etc.). The LLM
9
+ * is trained to treat tagged content as data, not instructions.
10
+ *
11
+ * 2. `wrapInstructions(text, uuid)` — emit instructions to the LLM that
12
+ * only "fire" when the same UUID is present in the user's most recent
13
+ * message. Pattern documented in docs/GUARDRAILS_MCP_RESPONSES.md.
14
+ *
15
+ * Tool returns:
16
+ * <instructions uuid="d8a3..."> follow steps... </instructions>
17
+ *
18
+ * The LLM, having been trained on this pattern, will refuse to follow
19
+ * the embedded instructions unless the user's prior message contains
20
+ * the UUID — proving the user genuinely wanted to authorize them.
21
+ *
22
+ * Use sparingly: prefer not to embed instructions in tool responses
23
+ * at all. When you must (multi-step workflows, follow-ups), use this
24
+ * wrapper.
25
+ */
26
+ import { randomUUID } from "node:crypto";
27
+ export function wrapUntrusted(text) {
28
+ return `<untrusted>\n${text}\n</untrusted>`;
29
+ }
30
+ export function wrapInstructions(text, uuid = randomUUID()) {
31
+ return {
32
+ text: `<instructions uuid="${uuid}">\n${text}\n</instructions>`,
33
+ uuid,
34
+ };
35
+ }
36
+ /**
37
+ * Wrap an actionable error message so the LLM gets a hint without
38
+ * treating the rest of the response as instructions.
39
+ */
40
+ export function wrapToolError(toolName, message, hint) {
41
+ let out = `Tool "${toolName}" failed: ${message}`;
42
+ if (hint)
43
+ out += `\nHint: ${hint}`;
44
+ return out;
45
+ }
46
+ //# sourceMappingURL=prompt-injection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-injection.js","sourceRoot":"","sources":["../src/prompt-injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,gBAAgB,IAAI,gBAAgB,CAAC;AAC9C,CAAC;AAOD,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,OAAe,UAAU,EAAE;IAE3B,OAAO;QACL,IAAI,EAAE,uBAAuB,IAAI,OAAO,IAAI,mBAAmB;QAC/D,IAAI;KACL,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAe,EAAE,IAAa;IAC5E,IAAI,GAAG,GAAG,SAAS,QAAQ,aAAa,OAAO,EAAE,CAAC;IAClD,IAAI,IAAI;QAAE,GAAG,IAAI,WAAW,IAAI,EAAE,CAAC;IACnC,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * MCP Resources — the second handler shape MCP clients can talk to.
3
+ *
4
+ * Tools run a verb ("call this function"). Resources expose addressable
5
+ * read-only state ("read this URI"). Hosts can subscribe to resources or
6
+ * browse them without re-invoking tools — useful for log streams, health
7
+ * snapshots, document content, etc.
8
+ *
9
+ * This kit mirrors the spec's two request shapes:
10
+ *
11
+ * - ListResourcesRequest / ListResourcesResult (concrete URIs)
12
+ * - ListResourceTemplatesRequest / ...Result (URI templates)
13
+ * - ReadResourceRequest / ReadResourceResult (resolve a URI)
14
+ *
15
+ * Consumers wire the returned `{ onList, onListTemplates, onRead }` into
16
+ * `server.setRequestHandler(...)`. Each callback handles its own error
17
+ * shape per the SDK contract.
18
+ *
19
+ * INVARIANT: like the tools dispatcher, every read goes through a perf
20
+ * span + structured error wrap. Never throw bare errors; the caller can
21
+ * inspect `error.message` for the resource URI + hint.
22
+ */
23
+ /** A directly-addressable resource (URI is the full address). */
24
+ export interface ResourceListEntry {
25
+ uri: string;
26
+ name: string;
27
+ description?: string;
28
+ mimeType?: string;
29
+ }
30
+ /** A URI template (e.g. "logs://recent/{n}") clients fill in to read. */
31
+ export interface ResourceTemplate {
32
+ uriTemplate: string;
33
+ name: string;
34
+ description?: string;
35
+ mimeType?: string;
36
+ }
37
+ /** Returned by the read callback when the URI is resolved. */
38
+ export interface ResourceContent {
39
+ uri: string;
40
+ mimeType: string;
41
+ /** UTF-8 text body. Mutually exclusive with `blob`. */
42
+ text?: string;
43
+ /** base64-encoded binary body. Mutually exclusive with `text`. */
44
+ blob?: string;
45
+ }
46
+ export interface ResourcesProvider {
47
+ /** Concrete resources to expose; default: none. */
48
+ list?(): Promise<ResourceListEntry[]> | ResourceListEntry[];
49
+ /** URI templates to expose; default: none. */
50
+ listTemplates?(): Promise<ResourceTemplate[]> | ResourceTemplate[];
51
+ /**
52
+ * Resolve a URI. Throw if unknown — the wrapper turns it into a
53
+ * structured error the SDK propagates to the client.
54
+ */
55
+ read(uri: string): Promise<ResourceContent> | ResourceContent;
56
+ }
57
+ export interface BuildResourcesHandlerOptions {
58
+ provider: ResourcesProvider;
59
+ /** Optional engine label surfaced in perf spans. */
60
+ engineLabel?: string;
61
+ }
62
+ export interface ResourceHandlers {
63
+ /** Wire into `server.setRequestHandler(ListResourcesRequestSchema, ...)`. */
64
+ onList(): Promise<{
65
+ resources: ResourceListEntry[];
66
+ }>;
67
+ /** Wire into `server.setRequestHandler(ListResourceTemplatesRequestSchema, ...)`. */
68
+ onListTemplates(): Promise<{
69
+ resourceTemplates: ResourceTemplate[];
70
+ }>;
71
+ /** Wire into `server.setRequestHandler(ReadResourceRequestSchema, ...)`. */
72
+ onRead(request: {
73
+ params: {
74
+ uri: string;
75
+ };
76
+ }): Promise<{
77
+ contents: ResourceContent[];
78
+ }>;
79
+ }
80
+ export declare function buildResourcesHandler(opts: BuildResourcesHandlerOptions): ResourceHandlers;
81
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,IAAI,CAAC,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,GAAG,iBAAiB,EAAE,CAAC;IAC5D,8CAA8C;IAC9C,aAAa,CAAC,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,GAAG,gBAAgB,EAAE,CAAC;IACnE;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,eAAe,CAAC;CAC/D;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAAC;IACtD,qFAAqF;IACrF,eAAe,IAAI,OAAO,CAAC;QAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAC,CAAC;IACtE,4EAA4E;IAC5E,MAAM,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC,CAAC;CACxF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,gBAAgB,CAwC1F"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * MCP Resources — the second handler shape MCP clients can talk to.
3
+ *
4
+ * Tools run a verb ("call this function"). Resources expose addressable
5
+ * read-only state ("read this URI"). Hosts can subscribe to resources or
6
+ * browse them without re-invoking tools — useful for log streams, health
7
+ * snapshots, document content, etc.
8
+ *
9
+ * This kit mirrors the spec's two request shapes:
10
+ *
11
+ * - ListResourcesRequest / ListResourcesResult (concrete URIs)
12
+ * - ListResourceTemplatesRequest / ...Result (URI templates)
13
+ * - ReadResourceRequest / ReadResourceResult (resolve a URI)
14
+ *
15
+ * Consumers wire the returned `{ onList, onListTemplates, onRead }` into
16
+ * `server.setRequestHandler(...)`. Each callback handles its own error
17
+ * shape per the SDK contract.
18
+ *
19
+ * INVARIANT: like the tools dispatcher, every read goes through a perf
20
+ * span + structured error wrap. Never throw bare errors; the caller can
21
+ * inspect `error.message` for the resource URI + hint.
22
+ */
23
+ import { perf } from "@george43g/robustness";
24
+ import { wrapToolError } from "./prompt-injection.js";
25
+ export function buildResourcesHandler(opts) {
26
+ const { provider, engineLabel = "ts" } = opts;
27
+ async function onList() {
28
+ const span = perf("resources.list");
29
+ try {
30
+ const resources = provider.list ? await provider.list() : [];
31
+ return { resources };
32
+ }
33
+ finally {
34
+ span.end();
35
+ }
36
+ }
37
+ async function onListTemplates() {
38
+ const span = perf("resources.listTemplates");
39
+ try {
40
+ const resourceTemplates = provider.listTemplates ? await provider.listTemplates() : [];
41
+ return { resourceTemplates };
42
+ }
43
+ finally {
44
+ span.end();
45
+ }
46
+ }
47
+ async function onRead(request) {
48
+ const uri = request.params.uri;
49
+ const span = perf(`resources.read[${engineLabel}]`);
50
+ try {
51
+ const content = await provider.read(uri);
52
+ return { contents: [content] };
53
+ }
54
+ catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ // wrapToolError produces a structured shape with an actionable
57
+ // hint — same pattern the tools dispatcher uses.
58
+ throw new Error(wrapToolError(`resources/read`, msg, `Unknown resource URI: "${uri}"`));
59
+ }
60
+ finally {
61
+ span.end();
62
+ }
63
+ }
64
+ return { onList, onListTemplates, onRead };
65
+ }
66
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAuDtD,MAAM,UAAU,qBAAqB,CAAC,IAAkC;IACtE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAE9C,KAAK,UAAU,MAAM;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,OAAO,EAAE,SAAS,EAAE,CAAC;QACvB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;IACH,CAAC;IAED,KAAK,UAAU,eAAe;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAC,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,iBAAiB,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvF,OAAO,EAAE,iBAAiB,EAAE,CAAC;QAC/B,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;IACH,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,OAAoC;QACxD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,WAAW,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACzC,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QACjC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,+DAA+D;YAC/D,iDAAiD;YACjD,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,gBAAgB,EAAE,GAAG,EAAE,0BAA0B,GAAG,GAAG,CAAC,CAAC,CAAC;QAC1F,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Sanitize untrusted user text before emitting via MCP or terminal.
3
+ *
4
+ * Lifted from imsg-mcp/src/sanitize.ts.
5
+ *
6
+ * - Strips ANSI CSI and OSC escape sequences to prevent terminal corruption.
7
+ * - Replaces NUL bytes and C0 control characters (except \n, \t, \r) with
8
+ * U+FFFD (Replacement Character) — keeps text scannable while making the
9
+ * substitution visible.
10
+ * - Truncates the string if it exceeds `maxLength` (default 4096),
11
+ * appending a horizontal ellipsis.
12
+ *
13
+ * Call this on every user-content surface in tool responses: chat
14
+ * snippets, search results, anything originally typed by a human or
15
+ * sourced from a system you don't control.
16
+ */
17
+ export declare function sanitize(text: string | null | undefined, maxLength?: number): string | null;
18
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,SAAS,SAAO,GAAG,MAAM,GAAG,IAAI,CAQzF"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Sanitize untrusted user text before emitting via MCP or terminal.
3
+ *
4
+ * Lifted from imsg-mcp/src/sanitize.ts.
5
+ *
6
+ * - Strips ANSI CSI and OSC escape sequences to prevent terminal corruption.
7
+ * - Replaces NUL bytes and C0 control characters (except \n, \t, \r) with
8
+ * U+FFFD (Replacement Character) — keeps text scannable while making the
9
+ * substitution visible.
10
+ * - Truncates the string if it exceeds `maxLength` (default 4096),
11
+ * appending a horizontal ellipsis.
12
+ *
13
+ * Call this on every user-content surface in tool responses: chat
14
+ * snippets, search results, anything originally typed by a human or
15
+ * sourced from a system you don't control.
16
+ */
17
+ const ANSI_REGEX = new RegExp([
18
+ "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
19
+ "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
20
+ ].join("|"), "g");
21
+ // Matches C0 control characters, excluding \t (0x09), \n (0x0A), and \r (0x0D)
22
+ const CONTROL_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
23
+ export function sanitize(text, maxLength = 4096) {
24
+ if (text == null)
25
+ return null;
26
+ let sanitized = text.replace(ANSI_REGEX, "");
27
+ sanitized = sanitized.replace(CONTROL_CHAR_REGEX, "�");
28
+ if (sanitized.length > maxLength) {
29
+ sanitized = `${sanitized.slice(0, maxLength - 1)}…`;
30
+ }
31
+ return sanitized;
32
+ }
33
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,UAAU,GAAG,IAAI,MAAM,CAC3B;IACE,8HAA8H;IAC9H,0DAA0D;CAC3D,CAAC,IAAI,CAAC,GAAG,CAAC,EACX,GAAG,CACJ,CAAC;AAEF,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,+BAA+B,CAAC;AAE3D,MAAM,UAAU,QAAQ,CAAC,IAA+B,EAAE,SAAS,GAAG,IAAI;IACxE,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC7C,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvD,IAAI,SAAS,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QACjC,SAAS,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}