@toolgrain/mcp 0.0.0-stage → 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 +85 -2
- package/dist/auth/abort.d.ts +3 -0
- package/dist/auth/abort.d.ts.map +1 -0
- package/dist/auth/abort.js +14 -0
- package/dist/auth/abort.js.map +1 -0
- package/dist/auth/audience.d.ts +3 -0
- package/dist/auth/audience.d.ts.map +1 -0
- package/dist/auth/audience.js +17 -0
- package/dist/auth/audience.js.map +1 -0
- package/dist/auth/authorization.d.ts +7 -0
- package/dist/auth/authorization.d.ts.map +1 -0
- package/dist/auth/authorization.js +58 -0
- package/dist/auth/authorization.js.map +1 -0
- package/dist/auth/context.d.ts +8 -0
- package/dist/auth/context.d.ts.map +1 -0
- package/dist/auth/context.js +37 -0
- package/dist/auth/context.js.map +1 -0
- package/dist/auth/errors.d.ts +14 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +23 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/types.d.ts +28 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/auth/url.d.ts +3 -0
- package/dist/auth/url.d.ts.map +1 -0
- package/dist/auth/url.js +18 -0
- package/dist/auth/url.js.map +1 -0
- package/dist/connection.d.ts +12 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +151 -0
- package/dist/connection.js.map +1 -0
- package/dist/context/access.d.ts +7 -0
- package/dist/context/access.d.ts.map +1 -0
- package/dist/context/access.js +22 -0
- package/dist/context/access.js.map +1 -0
- package/dist/context/definition.d.ts +24 -0
- package/dist/context/definition.d.ts.map +1 -0
- package/dist/context/definition.js +57 -0
- package/dist/context/definition.js.map +1 -0
- package/dist/context/registry.d.ts +5 -0
- package/dist/context/registry.d.ts.map +1 -0
- package/dist/context/registry.js +15 -0
- package/dist/context/registry.js.map +1 -0
- package/dist/context/scope.d.ts +5 -0
- package/dist/context/scope.d.ts.map +1 -0
- package/dist/context/scope.js +56 -0
- package/dist/context/scope.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +13 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dispatcher.d.ts +19 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +23 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/execute-request.d.ts +7 -0
- package/dist/execute-request.d.ts.map +1 -0
- package/dist/execute-request.js +20 -0
- package/dist/execute-request.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy/connection.d.ts +26 -0
- package/dist/legacy/connection.d.ts.map +1 -0
- package/dist/legacy/connection.js +73 -0
- package/dist/legacy/connection.js.map +1 -0
- package/dist/legacy/dispatcher.d.ts +10 -0
- package/dist/legacy/dispatcher.d.ts.map +1 -0
- package/dist/legacy/dispatcher.js +41 -0
- package/dist/legacy/dispatcher.js.map +1 -0
- package/dist/legacy/handlers.d.ts +12 -0
- package/dist/legacy/handlers.d.ts.map +1 -0
- package/dist/legacy/handlers.js +44 -0
- package/dist/legacy/handlers.js.map +1 -0
- package/dist/legacy/messages.d.ts +31 -0
- package/dist/legacy/messages.d.ts.map +1 -0
- package/dist/legacy/messages.js +27 -0
- package/dist/legacy/messages.js.map +1 -0
- package/dist/legacy/responses.d.ts +5 -0
- package/dist/legacy/responses.d.ts.map +1 -0
- package/dist/legacy/responses.js +8 -0
- package/dist/legacy/responses.js.map +1 -0
- package/dist/legacy/results.d.ts +5 -0
- package/dist/legacy/results.d.ts.map +1 -0
- package/dist/legacy/results.js +19 -0
- package/dist/legacy/results.js.map +1 -0
- package/dist/modern/dispatcher.d.ts +7 -0
- package/dist/modern/dispatcher.d.ts.map +1 -0
- package/dist/modern/dispatcher.js +37 -0
- package/dist/modern/dispatcher.js.map +1 -0
- package/dist/modern/handlers.d.ts +9 -0
- package/dist/modern/handlers.d.ts.map +1 -0
- package/dist/modern/handlers.js +36 -0
- package/dist/modern/handlers.js.map +1 -0
- package/dist/modern/messages.d.ts +142 -0
- package/dist/modern/messages.d.ts.map +1 -0
- package/dist/modern/messages.js +14 -0
- package/dist/modern/messages.js.map +1 -0
- package/dist/modern/metadata.d.ts +45 -0
- package/dist/modern/metadata.d.ts.map +1 -0
- package/dist/modern/metadata.js +49 -0
- package/dist/modern/metadata.js.map +1 -0
- package/dist/modern/responses.d.ts +3 -0
- package/dist/modern/responses.d.ts.map +1 -0
- package/dist/modern/responses.js +15 -0
- package/dist/modern/responses.js.map +1 -0
- package/dist/modern/results.d.ts +4 -0
- package/dist/modern/results.d.ts.map +1 -0
- package/dist/modern/results.js +4 -0
- package/dist/modern/results.js.map +1 -0
- package/dist/pending-requests.d.ts +12 -0
- package/dist/pending-requests.d.ts.map +1 -0
- package/dist/pending-requests.js +30 -0
- package/dist/pending-requests.js.map +1 -0
- package/dist/protocol/decode.d.ts +21 -0
- package/dist/protocol/decode.d.ts.map +1 -0
- package/dist/protocol/decode.js +40 -0
- package/dist/protocol/decode.js.map +1 -0
- package/dist/protocol/icon.d.ts +11 -0
- package/dist/protocol/icon.d.ts.map +1 -0
- package/dist/protocol/icon.js +8 -0
- package/dist/protocol/icon.js.map +1 -0
- package/dist/protocol/messages.d.ts +19 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +21 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol/responses.d.ts +9 -0
- package/dist/protocol/responses.d.ts.map +1 -0
- package/dist/protocol/responses.js +20 -0
- package/dist/protocol/responses.js.map +1 -0
- package/dist/protocol/version-metadata.d.ts +7 -0
- package/dist/protocol/version-metadata.d.ts.map +1 -0
- package/dist/protocol/version-metadata.js +21 -0
- package/dist/protocol/version-metadata.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +101 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/adapter.d.ts +12 -0
- package/dist/tools/adapter.d.ts.map +1 -0
- package/dist/tools/adapter.js +45 -0
- package/dist/tools/adapter.js.map +1 -0
- package/dist/tools/call.d.ts +8 -0
- package/dist/tools/call.d.ts.map +1 -0
- package/dist/tools/call.js +9 -0
- package/dist/tools/call.js.map +1 -0
- package/dist/tools/content.d.ts +181 -0
- package/dist/tools/content.d.ts.map +1 -0
- package/dist/tools/content.js +54 -0
- package/dist/tools/content.js.map +1 -0
- package/dist/tools/context.d.ts +36 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +50 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/definition.d.ts +55 -0
- package/dist/tools/definition.d.ts.map +1 -0
- package/dist/tools/definition.js +44 -0
- package/dist/tools/definition.js.map +1 -0
- package/dist/tools/errors.d.ts +11 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +19 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/json-value.d.ts +15 -0
- package/dist/tools/json-value.d.ts.map +1 -0
- package/dist/tools/json-value.js +42 -0
- package/dist/tools/json-value.js.map +1 -0
- package/dist/tools/name.d.ts +6 -0
- package/dist/tools/name.d.ts.map +1 -0
- package/dist/tools/name.js +13 -0
- package/dist/tools/name.js.map +1 -0
- package/dist/tools/registry.d.ts +14 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +31 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/results.d.ts +14 -0
- package/dist/tools/results.d.ts.map +1 -0
- package/dist/tools/results.js +71 -0
- package/dist/tools/results.js.map +1 -0
- package/dist/tools/rich-result.d.ts +44 -0
- package/dist/tools/rich-result.d.ts.map +1 -0
- package/dist/tools/rich-result.js +24 -0
- package/dist/tools/rich-result.js.map +1 -0
- package/dist/tools/schema.d.ts +18 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +70 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/transport-ownership.d.ts +4 -0
- package/dist/transport-ownership.d.ts.map +1 -0
- package/dist/transport-ownership.js +13 -0
- package/dist/transport-ownership.js.map +1 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +65 -4
- package/src/auth/abort.ts +14 -0
- package/src/auth/audience.ts +22 -0
- package/src/auth/authorization.ts +81 -0
- package/src/auth/context.ts +43 -0
- package/src/auth/errors.ts +35 -0
- package/src/auth/types.ts +34 -0
- package/src/auth/url.ts +22 -0
- package/src/connection.ts +207 -0
- package/src/context/access.ts +36 -0
- package/src/context/definition.ts +100 -0
- package/src/context/registry.ts +25 -0
- package/src/context/scope.ts +82 -0
- package/src/diagnostics.ts +25 -0
- package/src/dispatcher.ts +70 -0
- package/src/execute-request.ts +35 -0
- package/src/index.ts +41 -0
- package/src/legacy/connection.ts +101 -0
- package/src/legacy/dispatcher.ts +72 -0
- package/src/legacy/handlers.ts +76 -0
- package/src/legacy/messages.ts +27 -0
- package/src/legacy/responses.ts +20 -0
- package/src/legacy/results.ts +20 -0
- package/src/modern/dispatcher.ts +77 -0
- package/src/modern/handlers.ts +55 -0
- package/src/modern/messages.ts +14 -0
- package/src/modern/metadata.ts +49 -0
- package/src/modern/responses.ts +25 -0
- package/src/modern/results.ts +6 -0
- package/src/pending-requests.ts +30 -0
- package/src/protocol/decode.ts +69 -0
- package/src/protocol/icon.ts +8 -0
- package/src/protocol/messages.ts +29 -0
- package/src/protocol/responses.ts +27 -0
- package/src/protocol/version-metadata.ts +42 -0
- package/src/server.ts +164 -0
- package/src/tools/adapter.ts +76 -0
- package/src/tools/call.ts +18 -0
- package/src/tools/content.ts +57 -0
- package/src/tools/context.ts +119 -0
- package/src/tools/definition.ts +144 -0
- package/src/tools/errors.ts +21 -0
- package/src/tools/json-value.ts +81 -0
- package/src/tools/name.ts +13 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/results.ts +100 -0
- package/src/tools/rich-result.ts +70 -0
- package/src/tools/schema.ts +86 -0
- package/src/transport-ownership.ts +17 -0
- package/src/transport.ts +34 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Tool as ProtocolTool } from "@toolgrain/protocol";
|
|
2
|
+
import type * as core from "zod/v4/core";
|
|
3
|
+
import type { ToolContext } from "./context.js";
|
|
4
|
+
import type {
|
|
5
|
+
DeepReadonly,
|
|
6
|
+
IsJsonCompatible,
|
|
7
|
+
JsonCompatible,
|
|
8
|
+
} from "./json-value.js";
|
|
9
|
+
import type {
|
|
10
|
+
McpErrorResult,
|
|
11
|
+
McpResult,
|
|
12
|
+
McpStructuredResult,
|
|
13
|
+
} from "./rich-result.js";
|
|
14
|
+
|
|
15
|
+
export const TOOL_METADATA_KEYS = [
|
|
16
|
+
"title",
|
|
17
|
+
"description",
|
|
18
|
+
"annotations",
|
|
19
|
+
"icons",
|
|
20
|
+
"_meta",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type ToolMetadata = DeepReadonly<
|
|
24
|
+
Pick<ProtocolTool, (typeof TOOL_METADATA_KEYS)[number]>
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export interface ToolDefinition<
|
|
28
|
+
Schema extends core.$ZodType,
|
|
29
|
+
Result,
|
|
30
|
+
Output extends core.$ZodType = never,
|
|
31
|
+
> extends ToolMetadata {
|
|
32
|
+
readonly input: Schema;
|
|
33
|
+
readonly output?: Output;
|
|
34
|
+
/** Raw handler; server invocation performs validation and result conversion. */
|
|
35
|
+
readonly execute: (input: core.output<Schema>, ctx: ToolContext) => Result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Registered symbols let duplicate package copies recognize each other's tools.
|
|
39
|
+
const TOOL_BRAND = Symbol.for("@toolgrain/mcp/tool@1");
|
|
40
|
+
|
|
41
|
+
/** Internal erased type for heterogeneous, schema-checked registrations. */
|
|
42
|
+
export abstract class RegisteredTool {
|
|
43
|
+
abstract readonly input: core.$ZodType;
|
|
44
|
+
abstract readonly output?: core.$ZodType;
|
|
45
|
+
abstract readonly execute: (input: never, ctx: ToolContext) => unknown;
|
|
46
|
+
abstract readonly title?: string;
|
|
47
|
+
abstract readonly description?: string;
|
|
48
|
+
abstract readonly annotations?: NonNullable<ToolMetadata["annotations"]>;
|
|
49
|
+
abstract readonly icons?: NonNullable<ToolMetadata["icons"]>;
|
|
50
|
+
abstract readonly _meta?: NonNullable<ToolMetadata["_meta"]>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Structural brand shared by package copies. Object spreads drop prototype
|
|
54
|
+
* methods, so modified spreads must pass through tool() again.
|
|
55
|
+
*/
|
|
56
|
+
"~toolgrain"(): "tool@1" {
|
|
57
|
+
return "tool@1";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
Object.defineProperty(RegisteredTool.prototype, TOOL_BRAND, { value: true });
|
|
61
|
+
|
|
62
|
+
/** Recognize definitions created by any compatible copy of this package. */
|
|
63
|
+
export function isRegisteredTool(value: unknown): value is RegisteredTool {
|
|
64
|
+
return (
|
|
65
|
+
typeof value === "object" &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
(value as { [TOOL_BRAND]?: unknown })[TOOL_BRAND] === true
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Tool<
|
|
72
|
+
Schema extends core.$ZodType,
|
|
73
|
+
Result,
|
|
74
|
+
Output extends core.$ZodType = core.$ZodType,
|
|
75
|
+
> extends RegisteredTool {
|
|
76
|
+
declare readonly input: Schema;
|
|
77
|
+
declare readonly output?: Output;
|
|
78
|
+
declare readonly execute: (
|
|
79
|
+
input: core.output<Schema>,
|
|
80
|
+
ctx: ToolContext,
|
|
81
|
+
) => Result;
|
|
82
|
+
declare readonly title?: string;
|
|
83
|
+
declare readonly description?: string;
|
|
84
|
+
declare readonly annotations?: NonNullable<ToolMetadata["annotations"]>;
|
|
85
|
+
declare readonly icons?: NonNullable<ToolMetadata["icons"]>;
|
|
86
|
+
declare readonly _meta?: NonNullable<ToolMetadata["_meta"]>;
|
|
87
|
+
|
|
88
|
+
constructor(definition: ToolDefinition<Schema, Result, core.$ZodType>) {
|
|
89
|
+
super();
|
|
90
|
+
this.input = definition.input;
|
|
91
|
+
this.execute = definition.execute;
|
|
92
|
+
for (const key of ["output", ...TOOL_METADATA_KEYS] as const) {
|
|
93
|
+
const value = definition[key];
|
|
94
|
+
if (value !== undefined)
|
|
95
|
+
Object.defineProperty(this, key, { value, enumerable: true });
|
|
96
|
+
}
|
|
97
|
+
Object.freeze(this);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type OutputResult<Output extends core.$ZodType> =
|
|
102
|
+
| core.input<Output>
|
|
103
|
+
| McpStructuredResult<core.input<Output>>
|
|
104
|
+
| McpErrorResult<core.input<Output>>;
|
|
105
|
+
|
|
106
|
+
type JsonSafeOutput<Result> =
|
|
107
|
+
Result extends PromiseLike<infer Resolved>
|
|
108
|
+
? PromiseLike<JsonSafeOutput<Resolved>>
|
|
109
|
+
: Result extends McpResult<infer Structured>
|
|
110
|
+
? IsJsonCompatible<Structured> extends true
|
|
111
|
+
? Result
|
|
112
|
+
: never
|
|
113
|
+
: JsonCompatible<Result>;
|
|
114
|
+
|
|
115
|
+
type ToolDefinitionWithoutOutput<Schema extends core.$ZodType, Result> = Omit<
|
|
116
|
+
ToolDefinition<Schema, Result>,
|
|
117
|
+
"execute"
|
|
118
|
+
> & {
|
|
119
|
+
readonly execute: (
|
|
120
|
+
input: core.output<Schema>,
|
|
121
|
+
ctx: ToolContext,
|
|
122
|
+
) => Result & JsonSafeOutput<Result>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function tool<
|
|
126
|
+
Schema extends core.$ZodType,
|
|
127
|
+
Output extends core.$ZodType,
|
|
128
|
+
Result extends
|
|
129
|
+
| OutputResult<NoInfer<Output>>
|
|
130
|
+
| PromiseLike<OutputResult<NoInfer<Output>>>,
|
|
131
|
+
>(
|
|
132
|
+
definition: ToolDefinition<Schema, Result, Output> & {
|
|
133
|
+
readonly output: Output;
|
|
134
|
+
},
|
|
135
|
+
): Tool<Schema, Result, Output> & { readonly output: Output };
|
|
136
|
+
export function tool<Schema extends core.$ZodType, Result>(
|
|
137
|
+
definition: ToolDefinitionWithoutOutput<Schema, Result>,
|
|
138
|
+
): Tool<Schema, Result, never>;
|
|
139
|
+
/** Define a reusable MCP tool. Its execute property remains the raw handler. */
|
|
140
|
+
export function tool<Schema extends core.$ZodType, Result>(
|
|
141
|
+
definition: ToolDefinition<Schema, Result, core.$ZodType>,
|
|
142
|
+
): Tool<Schema, Result> {
|
|
143
|
+
return new Tool(definition);
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as core from "zod/v4/core";
|
|
2
|
+
|
|
3
|
+
export class UnknownToolError extends Error {
|
|
4
|
+
constructor(public readonly toolName: string) {
|
|
5
|
+
super(`Unknown tool: ${toolName}`);
|
|
6
|
+
this.name = "UnknownToolError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ToolInputError extends Error {
|
|
11
|
+
readonly issues: core.$ZodError["issues"];
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
public readonly toolName: string,
|
|
15
|
+
cause: core.$ZodError,
|
|
16
|
+
) {
|
|
17
|
+
super(`Invalid input for tool: ${toolName}`, { cause });
|
|
18
|
+
this.name = "ToolInputError";
|
|
19
|
+
this.issues = cause.issues;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { JSONValue } from "@toolgrain/protocol";
|
|
2
|
+
|
|
3
|
+
/** Readonly authoring views of protocol data; wire types remain unchanged. */
|
|
4
|
+
export type DeepReadonly<T> = T extends readonly (infer Item)[]
|
|
5
|
+
? readonly DeepReadonly<Item>[]
|
|
6
|
+
: T extends object
|
|
7
|
+
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
|
|
8
|
+
: T;
|
|
9
|
+
|
|
10
|
+
export type JsonCompatible<Value> = Value extends
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| boolean
|
|
14
|
+
| null
|
|
15
|
+
? Value
|
|
16
|
+
: Value extends (...args: never[]) => unknown
|
|
17
|
+
? never
|
|
18
|
+
: Value extends readonly (infer Item)[]
|
|
19
|
+
? readonly JsonCompatible<Item>[]
|
|
20
|
+
: Value extends object
|
|
21
|
+
? { [Key in keyof Value]: JsonCompatible<Value[Key]> }
|
|
22
|
+
: never;
|
|
23
|
+
|
|
24
|
+
export type IsJsonCompatible<Value> = [Value] extends [JsonCompatible<Value>]
|
|
25
|
+
? true
|
|
26
|
+
: false;
|
|
27
|
+
|
|
28
|
+
export interface AssertJSONOptions {
|
|
29
|
+
/** Treat undefined object properties as absent, matching JSON serialization. */
|
|
30
|
+
readonly omitUndefined?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function assertJSON(
|
|
34
|
+
value: unknown,
|
|
35
|
+
options: AssertJSONOptions = {},
|
|
36
|
+
ancestors = new Set<object>(),
|
|
37
|
+
): asserts value is JSONValue {
|
|
38
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
39
|
+
return;
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
41
|
+
if (typeof value !== "object" || value === null)
|
|
42
|
+
throw new Error("Tool output must be a JSON value");
|
|
43
|
+
if (ancestors.has(value)) throw new Error("Tool output contains a cycle");
|
|
44
|
+
const prototype = Object.getPrototypeOf(value);
|
|
45
|
+
if (
|
|
46
|
+
!Array.isArray(value) &&
|
|
47
|
+
prototype !== Object.prototype &&
|
|
48
|
+
prototype !== null
|
|
49
|
+
)
|
|
50
|
+
throw new Error("Tool output contains a non-JSON object instance");
|
|
51
|
+
ancestors.add(value);
|
|
52
|
+
try {
|
|
53
|
+
const keys = Reflect.ownKeys(value);
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (Array.isArray(value) && key === "length") continue;
|
|
56
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
57
|
+
if (
|
|
58
|
+
typeof key !== "string" ||
|
|
59
|
+
!descriptor?.enumerable ||
|
|
60
|
+
!("value" in descriptor)
|
|
61
|
+
)
|
|
62
|
+
throw new Error("Tool output contains a non-JSON property");
|
|
63
|
+
if (
|
|
64
|
+
Array.isArray(value) &&
|
|
65
|
+
(!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length)
|
|
66
|
+
)
|
|
67
|
+
throw new Error("Tool output contains a non-JSON array property");
|
|
68
|
+
if (
|
|
69
|
+
options.omitUndefined &&
|
|
70
|
+
!Array.isArray(value) &&
|
|
71
|
+
descriptor.value === undefined
|
|
72
|
+
)
|
|
73
|
+
continue;
|
|
74
|
+
assertJSON(descriptor.value, options, ancestors);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value) && keys.length !== value.length + 1)
|
|
77
|
+
throw new Error("Tool output contains a sparse array");
|
|
78
|
+
} finally {
|
|
79
|
+
ancestors.delete(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ToolNameError extends Error {
|
|
2
|
+
constructor(public readonly toolName: string) {
|
|
3
|
+
super(
|
|
4
|
+
`Invalid MCP tool name: ${JSON.stringify(toolName)}. Use 1–128 ASCII letters, digits, underscores, hyphens, or dots.`,
|
|
5
|
+
);
|
|
6
|
+
this.name = "ToolNameError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateToolName(name: string): void {
|
|
11
|
+
if (name.length < 1 || name.length > 128 || /[^A-Za-z0-9_.-]/.test(name))
|
|
12
|
+
throw new ToolNameError(name);
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as core from "zod/v4/core";
|
|
2
|
+
import type { ToolContext } from "./context.js";
|
|
3
|
+
import {
|
|
4
|
+
isRegisteredTool,
|
|
5
|
+
type RegisteredTool,
|
|
6
|
+
type ToolMetadata,
|
|
7
|
+
} from "./definition.js";
|
|
8
|
+
import { ToolInputError, UnknownToolError } from "./errors.js";
|
|
9
|
+
|
|
10
|
+
export type ToolMap = Record<string, RegisteredTool>;
|
|
11
|
+
|
|
12
|
+
export interface ToolEntry extends ToolMetadata {
|
|
13
|
+
name: string;
|
|
14
|
+
input: RegisteredTool["input"];
|
|
15
|
+
output?: NonNullable<RegisteredTool["output"]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Registry {
|
|
19
|
+
list(): ToolEntry[];
|
|
20
|
+
execute(name: string, input: unknown, ctx: ToolContext): Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createRegistry(tools: ToolMap): Registry {
|
|
24
|
+
const entries = new Map(
|
|
25
|
+
Object.entries(tools).map(([name, definition]) => {
|
|
26
|
+
if (!isRegisteredTool(definition))
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`Tool ${JSON.stringify(name)} must be created with tool()`,
|
|
29
|
+
);
|
|
30
|
+
return [name, Object.freeze({ ...definition })] as const;
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
list() {
|
|
36
|
+
return Array.from(entries, ([name, definition]) => {
|
|
37
|
+
const { execute: _execute, ...metadata } = definition;
|
|
38
|
+
return { name, ...metadata };
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async execute(name, input, ctx) {
|
|
42
|
+
const definition = entries.get(name);
|
|
43
|
+
if (!definition) throw new UnknownToolError(name);
|
|
44
|
+
const parsed = await core.safeParseAsync(definition.input, input);
|
|
45
|
+
if (!parsed.success) throw new ToolInputError(name, parsed.error);
|
|
46
|
+
// A request cancelled during validation never starts its handler.
|
|
47
|
+
ctx.signal.throwIfAborted();
|
|
48
|
+
// Parsing with this definition's schema establishes its handler input type.
|
|
49
|
+
return definition.execute(parsed.data as never, ctx);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ContentBlock, MetaObject } from "@toolgrain/protocol";
|
|
2
|
+
import * as core from "zod/v4/core";
|
|
3
|
+
import { explicitResult } from "./content.js";
|
|
4
|
+
import { type AssertJSONOptions, assertJSON } from "./json-value.js";
|
|
5
|
+
import { isMcpResult, type McpResult } from "./rich-result.js";
|
|
6
|
+
|
|
7
|
+
/** Validated output shared by both protocol adapters. */
|
|
8
|
+
export interface ToolExecutionResult {
|
|
9
|
+
content: ContentBlock[];
|
|
10
|
+
structuredContent?: unknown;
|
|
11
|
+
isError?: boolean;
|
|
12
|
+
_meta?: MetaObject;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toolResult(value: unknown): ToolExecutionResult {
|
|
16
|
+
if (isMcpResult(value)) return withStructuredText(readExplicit(value));
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return { content: [{ type: "text", text: value }] };
|
|
19
|
+
return structuredResult(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function structuredResult(
|
|
23
|
+
value: unknown,
|
|
24
|
+
options?: AssertJSONOptions,
|
|
25
|
+
): ToolExecutionResult {
|
|
26
|
+
assertJSON(value, options);
|
|
27
|
+
const text = JSON.stringify(value);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text }],
|
|
30
|
+
structuredContent: JSON.parse(text),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readExplicit(value: McpResult<unknown>): ToolExecutionResult {
|
|
35
|
+
// Structured content is application data, so undefined values inside it fail.
|
|
36
|
+
const { structuredContent } = value.value;
|
|
37
|
+
if (structuredContent !== undefined) assertJSON(structuredContent);
|
|
38
|
+
// Undefined optional envelope fields, such as `isError: undefined`, are absent.
|
|
39
|
+
assertJSON(value.value, { omitUndefined: true });
|
|
40
|
+
return explicitResult.parse(
|
|
41
|
+
JSON.parse(JSON.stringify(value.value)),
|
|
42
|
+
) as ToolExecutionResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function withStructuredText(result: ToolExecutionResult): ToolExecutionResult {
|
|
46
|
+
if (Object.hasOwn(result, "structuredContent")) {
|
|
47
|
+
const text = JSON.stringify(result.structuredContent);
|
|
48
|
+
if (
|
|
49
|
+
!result.content.some(
|
|
50
|
+
(block) => block.type === "text" && block.text === text,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
result.content.push({ type: "text", text });
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Validate supplied structured data, including data in explicit error results. */
|
|
59
|
+
export async function resolveToolResult(
|
|
60
|
+
name: string,
|
|
61
|
+
value: unknown,
|
|
62
|
+
output?: core.$ZodType,
|
|
63
|
+
): Promise<ToolExecutionResult> {
|
|
64
|
+
if (!output) return toolResult(value);
|
|
65
|
+
const explicit = isMcpResult(value) ? readExplicit(value) : undefined;
|
|
66
|
+
if (explicit && !Object.hasOwn(explicit, "structuredContent")) {
|
|
67
|
+
if (explicit.isError) return explicit;
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Output schema requires structuredContent for tool: ${name}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const parsed = await core.safeParseAsync(
|
|
73
|
+
output,
|
|
74
|
+
explicit ? explicit.structuredContent : value,
|
|
75
|
+
);
|
|
76
|
+
if (!parsed.success)
|
|
77
|
+
throw new Error(`Invalid output for tool: ${name}`, {
|
|
78
|
+
cause: parsed.error,
|
|
79
|
+
});
|
|
80
|
+
// The schema accepted these values; undefined optional properties are absent.
|
|
81
|
+
const result = structuredResult(parsed.data, { omitUndefined: true });
|
|
82
|
+
if (!explicit) return result;
|
|
83
|
+
// Text serializing the raw data describes the parsed data after output parsing.
|
|
84
|
+
const raw = JSON.stringify(explicit.structuredContent);
|
|
85
|
+
const text = JSON.stringify(result.structuredContent);
|
|
86
|
+
return withStructuredText({
|
|
87
|
+
...explicit,
|
|
88
|
+
content: explicit.content.map((block) =>
|
|
89
|
+
block.type === "text" && block.text === raw ? { ...block, text } : block,
|
|
90
|
+
),
|
|
91
|
+
structuredContent: result.structuredContent,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toolError(message: string): ToolExecutionResult {
|
|
96
|
+
return {
|
|
97
|
+
isError: true,
|
|
98
|
+
content: [{ type: "text", text: message }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ContentBlock, MetaObject } from "@toolgrain/protocol";
|
|
2
|
+
import type { DeepReadonly, JsonCompatible } from "./json-value.js";
|
|
3
|
+
|
|
4
|
+
export interface McpResultOptions<Structured = unknown> {
|
|
5
|
+
content: readonly DeepReadonly<ContentBlock>[];
|
|
6
|
+
structuredContent?: Structured & JsonCompatible<NoInfer<Structured>>;
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
_meta?: MetaObject;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Registered symbols let duplicate package copies recognize each other's results.
|
|
12
|
+
const RESULT_BRAND = Symbol.for("@toolgrain/mcp/result@1");
|
|
13
|
+
|
|
14
|
+
export class McpResult<Structured> {
|
|
15
|
+
constructor(readonly value: McpResultOptions<Structured>) {
|
|
16
|
+
Object.freeze(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Structural brand shared by package copies; object spreads do not keep it. */
|
|
20
|
+
"~toolgrain"(): "result@1" {
|
|
21
|
+
return "result@1";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(McpResult.prototype, RESULT_BRAND, { value: true });
|
|
25
|
+
|
|
26
|
+
/** Recognize results created by any compatible copy of this package. */
|
|
27
|
+
export function isMcpResult(value: unknown): value is McpResult<unknown> {
|
|
28
|
+
return (
|
|
29
|
+
typeof value === "object" &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
(value as { [RESULT_BRAND]?: unknown })[RESULT_BRAND] === true
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type McpErrorResult<Structured = never> = McpResult<Structured> & {
|
|
36
|
+
readonly value: McpResultOptions<Structured> & { isError: true };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A result whose structured data is present, as required by an output schema. */
|
|
40
|
+
export type McpStructuredResult<Structured> = McpResult<Structured> & {
|
|
41
|
+
readonly value: McpResultOptions<Structured> & {
|
|
42
|
+
structuredContent: Structured;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Explicitly return MCP content. Ordinary objects always remain application data. */
|
|
47
|
+
export function mcpResult<Structured = never>(
|
|
48
|
+
value: McpResultOptions<Structured> & {
|
|
49
|
+
isError: true;
|
|
50
|
+
structuredContent: Structured;
|
|
51
|
+
},
|
|
52
|
+
): McpErrorResult<Structured> & McpStructuredResult<Structured>;
|
|
53
|
+
export function mcpResult(
|
|
54
|
+
value: McpResultOptions<never> & { isError: true },
|
|
55
|
+
): McpErrorResult<never>;
|
|
56
|
+
export function mcpResult<Structured>(
|
|
57
|
+
value: McpResultOptions<Structured> & { isError: true },
|
|
58
|
+
): McpErrorResult<Structured>;
|
|
59
|
+
export function mcpResult<Structured>(
|
|
60
|
+
value: McpResultOptions<Structured> & { structuredContent: Structured },
|
|
61
|
+
): McpStructuredResult<Structured>;
|
|
62
|
+
export function mcpResult(value: McpResultOptions<never>): McpResult<never>;
|
|
63
|
+
export function mcpResult<Structured = never>(
|
|
64
|
+
value: McpResultOptions<Structured>,
|
|
65
|
+
): McpResult<Structured>;
|
|
66
|
+
export function mcpResult<Structured = never>(
|
|
67
|
+
value: McpResultOptions<Structured>,
|
|
68
|
+
): McpResult<Structured> {
|
|
69
|
+
return new McpResult(value);
|
|
70
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Tool as ProtocolTool } from "@toolgrain/protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as core from "zod/v4/core";
|
|
4
|
+
import { icon } from "../protocol/icon.js";
|
|
5
|
+
import { object } from "../protocol/messages.js";
|
|
6
|
+
import { TOOL_METADATA_KEYS } from "./definition.js";
|
|
7
|
+
import { assertJSON } from "./json-value.js";
|
|
8
|
+
import { validateToolName } from "./name.js";
|
|
9
|
+
import type { ToolMap } from "./registry.js";
|
|
10
|
+
|
|
11
|
+
export const toolAnnotations = z.looseObject({
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
readOnlyHint: z.boolean().optional(),
|
|
14
|
+
destructiveHint: z.boolean().optional(),
|
|
15
|
+
idempotentHint: z.boolean().optional(),
|
|
16
|
+
openWorldHint: z.boolean().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const metadata = z.strictObject({
|
|
20
|
+
title: z.string().optional(),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
icons: z.array(icon).optional(),
|
|
23
|
+
annotations: toolAnnotations.optional(),
|
|
24
|
+
_meta: object.optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export class ToolSchemaError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
public readonly toolName: string,
|
|
30
|
+
cause: unknown,
|
|
31
|
+
public readonly schemaKind: "input" | "output" | "metadata" = "input",
|
|
32
|
+
) {
|
|
33
|
+
super(`Cannot expose ${schemaKind} schema for tool: ${toolName}`, {
|
|
34
|
+
cause,
|
|
35
|
+
});
|
|
36
|
+
this.name = "ToolSchemaError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function describeTool(
|
|
41
|
+
name: string,
|
|
42
|
+
definition: Pick<
|
|
43
|
+
ToolMap[string],
|
|
44
|
+
| "input"
|
|
45
|
+
| "output"
|
|
46
|
+
| "title"
|
|
47
|
+
| "description"
|
|
48
|
+
| "icons"
|
|
49
|
+
| "annotations"
|
|
50
|
+
| "_meta"
|
|
51
|
+
>,
|
|
52
|
+
): ProtocolTool {
|
|
53
|
+
validateToolName(name);
|
|
54
|
+
let schemaKind: "input" | "output" | "metadata" = "input";
|
|
55
|
+
try {
|
|
56
|
+
const inputSchema = core.toJSONSchema(definition.input, {
|
|
57
|
+
io: "input",
|
|
58
|
+
target: "draft-2020-12",
|
|
59
|
+
});
|
|
60
|
+
if (inputSchema.type !== "object")
|
|
61
|
+
throw new Error("Tool input schema must have an object root");
|
|
62
|
+
schemaKind = "output";
|
|
63
|
+
const outputSchema =
|
|
64
|
+
definition.output === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: core.toJSONSchema(definition.output, {
|
|
67
|
+
io: "output",
|
|
68
|
+
target: "draft-2020-12",
|
|
69
|
+
});
|
|
70
|
+
schemaKind = "metadata";
|
|
71
|
+
const fields: Record<string, unknown> = {};
|
|
72
|
+
for (const key of TOOL_METADATA_KEYS)
|
|
73
|
+
if (definition[key] !== undefined) fields[key] = definition[key];
|
|
74
|
+
assertJSON(fields);
|
|
75
|
+
metadata.parse(fields);
|
|
76
|
+
const result = {
|
|
77
|
+
name,
|
|
78
|
+
inputSchema: { ...inputSchema, type: "object" },
|
|
79
|
+
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
80
|
+
...fields,
|
|
81
|
+
};
|
|
82
|
+
return structuredClone(result) as ProtocolTool;
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
throw new ToolSchemaError(name, cause, schemaKind);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RequestTransport, Transport } from "./transport.js";
|
|
2
|
+
|
|
3
|
+
const key = Symbol.for("@toolgrain/mcp/claimed-transports/v1");
|
|
4
|
+
|
|
5
|
+
/** Claim once before startup. Share weak ownership across compatible package copies. */
|
|
6
|
+
export function claimTransport(transport: Transport | RequestTransport): void {
|
|
7
|
+
let claimed = Object.getOwnPropertyDescriptor(globalThis, key)?.value as
|
|
8
|
+
| WeakSet<object>
|
|
9
|
+
| undefined;
|
|
10
|
+
if (!claimed) {
|
|
11
|
+
claimed = new WeakSet<object>();
|
|
12
|
+
Object.defineProperty(globalThis, key, { value: claimed });
|
|
13
|
+
}
|
|
14
|
+
if (claimed.has(transport))
|
|
15
|
+
throw new Error("Transport is already attached to a server");
|
|
16
|
+
claimed.add(transport);
|
|
17
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ProtocolResponse } from "@toolgrain/protocol";
|
|
2
|
+
import type { Server } from "./server.js";
|
|
3
|
+
|
|
4
|
+
/** Callbacks are synchronous; message processing runs independently. */
|
|
5
|
+
export interface TransportCallbacks {
|
|
6
|
+
onMessage(message: unknown): void;
|
|
7
|
+
onParseError(error: unknown): void;
|
|
8
|
+
onError(error: unknown): void;
|
|
9
|
+
onClose(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Single-use adapters own UTF-8 framing, JSON decoding, and stream cleanup. */
|
|
13
|
+
export interface Transport {
|
|
14
|
+
readonly mode?: "message";
|
|
15
|
+
start(callbacks: TransportCallbacks): Promise<void>;
|
|
16
|
+
send(message: ProtocolResponse): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Single-use request transports own listeners and use per-request authorization. */
|
|
21
|
+
export interface RequestTransport {
|
|
22
|
+
readonly mode: "request";
|
|
23
|
+
start(
|
|
24
|
+
server: Server,
|
|
25
|
+
callbacks: Pick<TransportCallbacks, "onError" | "onClose">,
|
|
26
|
+
): Promise<void>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRequestTransport(
|
|
31
|
+
transport: Transport | RequestTransport,
|
|
32
|
+
): transport is RequestTransport {
|
|
33
|
+
return "mode" in transport && transport.mode === "request";
|
|
34
|
+
}
|