@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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/dispatch.d.ts +58 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +110 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt-injection.d.ts +37 -0
- package/dist/prompt-injection.d.ts.map +1 -0
- package/dist/prompt-injection.js +46 -0
- package/dist/prompt-injection.js.map +1 -0
- package/dist/resources.d.ts +81 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +66 -0
- package/dist/resources.js.map +1 -0
- package/dist/sanitize.d.ts +18 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +33 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/tool-registry.d.ts +78 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +38 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/transports/http.d.ts +47 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +150 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/stdio.d.ts +17 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +53 -0
- package/dist/transports/stdio.js.map +1 -0
- package/package.json +72 -0
- package/src/dispatch.ts +172 -0
- package/src/index.ts +34 -0
- package/src/prompt-injection.ts +56 -0
- package/src/resources.ts +120 -0
- package/src/sanitize.ts +37 -0
- package/src/tool-registry.ts +111 -0
- package/src/transports/http.ts +188 -0
- 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"}
|
package/dist/dispatch.js
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/sanitize.js
ADDED
|
@@ -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"}
|