@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,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INVALID_REQUEST,
|
|
3
|
+
type JSONRPCErrorResponse,
|
|
4
|
+
type RequestId,
|
|
5
|
+
} from "@toolgrain/protocol";
|
|
6
|
+
import {
|
|
7
|
+
cancellation,
|
|
8
|
+
envelope,
|
|
9
|
+
type Notification,
|
|
10
|
+
type Request,
|
|
11
|
+
requestId,
|
|
12
|
+
} from "./messages.js";
|
|
13
|
+
import { protocolError } from "./responses.js";
|
|
14
|
+
|
|
15
|
+
type Incoming =
|
|
16
|
+
| { kind: "request"; request: Request }
|
|
17
|
+
| { kind: "cancel"; id: RequestId }
|
|
18
|
+
| { kind: "notification"; notification: Notification }
|
|
19
|
+
| { kind: "ignore" }
|
|
20
|
+
| {
|
|
21
|
+
kind: "error";
|
|
22
|
+
response: JSONRPCErrorResponse;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Decode the envelope; method parameters are validated by the dispatcher. */
|
|
26
|
+
export function decode(message: unknown): Incoming {
|
|
27
|
+
const parsed = envelope.safeParse(message);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
if (
|
|
30
|
+
typeof message === "object" &&
|
|
31
|
+
message !== null &&
|
|
32
|
+
!Array.isArray(message) &&
|
|
33
|
+
(("method" in message && !Object.hasOwn(message, "id")) ||
|
|
34
|
+
isResponse(message))
|
|
35
|
+
)
|
|
36
|
+
return { kind: "ignore" };
|
|
37
|
+
const id =
|
|
38
|
+
typeof message === "object" && message !== null && "id" in message
|
|
39
|
+
? requestId.safeParse(message.id)
|
|
40
|
+
: undefined;
|
|
41
|
+
return {
|
|
42
|
+
kind: "error",
|
|
43
|
+
response: protocolError(
|
|
44
|
+
INVALID_REQUEST,
|
|
45
|
+
"Invalid request",
|
|
46
|
+
id?.success ? id.data : undefined,
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (parsed.data.id !== undefined)
|
|
51
|
+
return { kind: "request", request: { ...parsed.data, id: parsed.data.id } };
|
|
52
|
+
if (parsed.data.method === "notifications/cancelled") {
|
|
53
|
+
const params = cancellation.safeParse(parsed.data.params);
|
|
54
|
+
if (params.success) return { kind: "cancel", id: params.data.requestId };
|
|
55
|
+
return { kind: "ignore" };
|
|
56
|
+
}
|
|
57
|
+
return { kind: "notification", notification: parsed.data };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Servers send no requests, so a client response has nothing to answer. */
|
|
61
|
+
function isResponse(message: object): boolean {
|
|
62
|
+
return (
|
|
63
|
+
"jsonrpc" in message &&
|
|
64
|
+
message.jsonrpc === "2.0" &&
|
|
65
|
+
!Object.hasOwn(message, "method") &&
|
|
66
|
+
Object.hasOwn(message, "id") &&
|
|
67
|
+
Object.hasOwn(message, "result") !== Object.hasOwn(message, "error")
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { JSONRPC_VERSION } from "@toolgrain/protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const object = z.record(z.string(), z.unknown());
|
|
5
|
+
export const requestId = z.union([
|
|
6
|
+
z.string(),
|
|
7
|
+
z.number().finite().refine(Number.isInteger, "Request ID must be an integer"),
|
|
8
|
+
]);
|
|
9
|
+
export const envelope = z
|
|
10
|
+
.looseObject({
|
|
11
|
+
jsonrpc: z.literal(JSONRPC_VERSION),
|
|
12
|
+
method: z.string(),
|
|
13
|
+
id: requestId.optional(),
|
|
14
|
+
params: object.optional(),
|
|
15
|
+
})
|
|
16
|
+
.refine(
|
|
17
|
+
(value) =>
|
|
18
|
+
!Object.hasOwn(value, "result") && !Object.hasOwn(value, "error"),
|
|
19
|
+
);
|
|
20
|
+
export const cancellation = z.looseObject({
|
|
21
|
+
requestId,
|
|
22
|
+
reason: z.string().optional(),
|
|
23
|
+
_meta: object.optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type Request = z.infer<typeof envelope> & {
|
|
27
|
+
id: z.infer<typeof requestId>;
|
|
28
|
+
};
|
|
29
|
+
export type Notification = z.infer<typeof envelope>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { JSONRPCErrorResponse, RequestId } from "@toolgrain/protocol";
|
|
2
|
+
import { JSONRPC_VERSION } from "@toolgrain/protocol";
|
|
3
|
+
|
|
4
|
+
export function protocolError(
|
|
5
|
+
code: number,
|
|
6
|
+
message: string,
|
|
7
|
+
id?: RequestId,
|
|
8
|
+
data?: unknown,
|
|
9
|
+
): JSONRPCErrorResponse {
|
|
10
|
+
return {
|
|
11
|
+
jsonrpc: JSONRPC_VERSION,
|
|
12
|
+
...(id === undefined ? {} : { id }),
|
|
13
|
+
error: { code, message, ...(data === undefined ? {} : { data }) },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** An internal dispatch failure, converted to a protocol response at the connection boundary. */
|
|
18
|
+
export class ProtocolError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly code: number,
|
|
21
|
+
message: string,
|
|
22
|
+
public readonly data?: unknown,
|
|
23
|
+
) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ProtocolError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INVALID_PARAMS,
|
|
3
|
+
type SupportedProtocolVersion,
|
|
4
|
+
} from "@toolgrain/protocol";
|
|
5
|
+
import type { Request } from "./messages.js";
|
|
6
|
+
import { ProtocolError } from "./responses.js";
|
|
7
|
+
|
|
8
|
+
export function hasProtocolVersionMetadata(request: Request): boolean {
|
|
9
|
+
const meta = request.params?._meta;
|
|
10
|
+
return (
|
|
11
|
+
typeof meta === "object" &&
|
|
12
|
+
meta !== null &&
|
|
13
|
+
Object.hasOwn(meta, "io.modelcontextprotocol/protocolVersion")
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function metadataMatchesVersion(
|
|
18
|
+
request: Request,
|
|
19
|
+
version: SupportedProtocolVersion,
|
|
20
|
+
): boolean {
|
|
21
|
+
const meta = request.params?._meta as Record<string, unknown>;
|
|
22
|
+
return (
|
|
23
|
+
hasProtocolVersionMetadata(request) &&
|
|
24
|
+
meta["io.modelcontextprotocol/protocolVersion"] === version
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A transport-selected revision cannot contradict an explicit request revision. */
|
|
29
|
+
export function validateVersionContext(
|
|
30
|
+
request: Request,
|
|
31
|
+
version?: SupportedProtocolVersion,
|
|
32
|
+
): void {
|
|
33
|
+
if (
|
|
34
|
+
version !== undefined &&
|
|
35
|
+
hasProtocolVersionMetadata(request) &&
|
|
36
|
+
!metadataMatchesVersion(request, version)
|
|
37
|
+
)
|
|
38
|
+
throw new ProtocolError(
|
|
39
|
+
INVALID_PARAMS,
|
|
40
|
+
"Protocol version does not match request metadata",
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProtocolResponse,
|
|
3
|
+
Tool as ProtocolTool,
|
|
4
|
+
SupportedProtocolVersion,
|
|
5
|
+
} from "@toolgrain/protocol";
|
|
6
|
+
import { createAuthorization } from "./auth/authorization.js";
|
|
7
|
+
import { AuthError, isAuthError } from "./auth/errors.js";
|
|
8
|
+
import type { AuthMetadata, AuthProvider, AuthScope } from "./auth/types.js";
|
|
9
|
+
import { createConnection } from "./connection.js";
|
|
10
|
+
import type { RegisteredContextProvider } from "./context/definition.js";
|
|
11
|
+
import { createContextRegistry } from "./context/registry.js";
|
|
12
|
+
import { type ErrorHandler, reportError } from "./diagnostics.js";
|
|
13
|
+
import { createDispatcher, formatErrorResponse } from "./dispatcher.js";
|
|
14
|
+
import { executeRequest } from "./execute-request.js";
|
|
15
|
+
import { decode } from "./protocol/decode.js";
|
|
16
|
+
import { createToolAdapter } from "./tools/adapter.js";
|
|
17
|
+
import type { ToolMap } from "./tools/registry.js";
|
|
18
|
+
import {
|
|
19
|
+
isRequestTransport,
|
|
20
|
+
type RequestTransport,
|
|
21
|
+
type Transport,
|
|
22
|
+
} from "./transport.js";
|
|
23
|
+
|
|
24
|
+
export interface ServerOptions<Tools extends ToolMap = ToolMap> {
|
|
25
|
+
name: string;
|
|
26
|
+
version: string;
|
|
27
|
+
tools: Tools;
|
|
28
|
+
instructions?: string;
|
|
29
|
+
/** Tool failures include frozen tool/request details; other diagnostics omit them. */
|
|
30
|
+
onError?: ErrorHandler;
|
|
31
|
+
auth?: AuthProvider;
|
|
32
|
+
context?: readonly RegisteredContextProvider[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HandleOptions {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
protocolVersion?: SupportedProtocolVersion;
|
|
38
|
+
auth?: AuthScope;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Server {
|
|
42
|
+
handle(
|
|
43
|
+
message: unknown,
|
|
44
|
+
options?: HandleOptions,
|
|
45
|
+
): Promise<ProtocolResponse | undefined>;
|
|
46
|
+
getToolDefinitions(): ProtocolTool[];
|
|
47
|
+
getAuthMetadata(): AuthMetadata | undefined;
|
|
48
|
+
authorize(
|
|
49
|
+
accessToken: string,
|
|
50
|
+
options: { resource: string; signal?: AbortSignal },
|
|
51
|
+
): Promise<AuthScope>;
|
|
52
|
+
listen(transport: Transport | RequestTransport): Promise<void>;
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Compose the tool adapter, MCP dispatcher, and transport connection. */
|
|
57
|
+
export function createServer<const Tools extends ToolMap>(
|
|
58
|
+
options: ServerOptions<Tools>,
|
|
59
|
+
): Server {
|
|
60
|
+
const authorization = createAuthorization(options.auth);
|
|
61
|
+
const tools = createToolAdapter(options.tools, options.onError);
|
|
62
|
+
const dispatch = createDispatcher(
|
|
63
|
+
tools,
|
|
64
|
+
{ name: options.name, version: options.version },
|
|
65
|
+
options.instructions,
|
|
66
|
+
createContextRegistry(options.context),
|
|
67
|
+
);
|
|
68
|
+
let closed = false;
|
|
69
|
+
const running = new Set<AbortController>();
|
|
70
|
+
const closeIndependent = () => {
|
|
71
|
+
if (closed) return;
|
|
72
|
+
closed = true;
|
|
73
|
+
for (const controller of running)
|
|
74
|
+
controller.abort(new DOMException("Server closed", "AbortError"));
|
|
75
|
+
running.clear();
|
|
76
|
+
};
|
|
77
|
+
const connection = createConnection(
|
|
78
|
+
dispatch,
|
|
79
|
+
options.onError,
|
|
80
|
+
closeIndependent,
|
|
81
|
+
);
|
|
82
|
+
const requestController = (signal?: AbortSignal) => {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const abort = () => controller.abort(signal?.reason);
|
|
85
|
+
if (signal?.aborted) abort();
|
|
86
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
87
|
+
running.add(controller);
|
|
88
|
+
return {
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
finish() {
|
|
91
|
+
signal?.removeEventListener("abort", abort);
|
|
92
|
+
running.delete(controller);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const server: Server = {
|
|
97
|
+
async listen(transport) {
|
|
98
|
+
if (isRequestTransport(transport))
|
|
99
|
+
return connection.listenRequests(transport, server);
|
|
100
|
+
if (authorization.metadata())
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Auth-configured servers require HTTP and cannot listen on a message transport",
|
|
103
|
+
);
|
|
104
|
+
return connection.listen(transport);
|
|
105
|
+
},
|
|
106
|
+
getToolDefinitions: () => tools.list(),
|
|
107
|
+
getAuthMetadata: authorization.metadata,
|
|
108
|
+
async authorize(accessToken, { resource, signal }) {
|
|
109
|
+
if (closed) throw new AuthError("temporarily_unavailable");
|
|
110
|
+
const pending = requestController(signal);
|
|
111
|
+
try {
|
|
112
|
+
return await authorization.authorize(
|
|
113
|
+
accessToken,
|
|
114
|
+
resource,
|
|
115
|
+
pending.signal,
|
|
116
|
+
pending.finish,
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
pending.finish();
|
|
120
|
+
if (
|
|
121
|
+
!pending.signal.aborted &&
|
|
122
|
+
(!isAuthError(error) || error.code === "temporarily_unavailable")
|
|
123
|
+
)
|
|
124
|
+
reportError(options.onError, error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async handle(
|
|
129
|
+
message,
|
|
130
|
+
{ signal, protocolVersion, auth }: HandleOptions = {},
|
|
131
|
+
) {
|
|
132
|
+
if (closed) throw new Error("Server is closed");
|
|
133
|
+
const identity = authorization.consume(auth);
|
|
134
|
+
if (auth?.signal.aborted) return;
|
|
135
|
+
const active = () => !closed && !signal?.aborted;
|
|
136
|
+
if (!active()) return;
|
|
137
|
+
const incoming = decode(message);
|
|
138
|
+
if (incoming.kind === "error")
|
|
139
|
+
return formatErrorResponse(incoming.response, protocolVersion);
|
|
140
|
+
if (incoming.kind !== "request") return;
|
|
141
|
+
const pending = requestController(signal);
|
|
142
|
+
// The authorization signal covers verification and body decoding too.
|
|
143
|
+
const combined = auth
|
|
144
|
+
? AbortSignal.any([pending.signal, auth.signal])
|
|
145
|
+
: pending.signal;
|
|
146
|
+
try {
|
|
147
|
+
return await executeRequest(
|
|
148
|
+
dispatch,
|
|
149
|
+
incoming.request,
|
|
150
|
+
{ signal: combined, ...(identity ? { auth: identity } : {}) },
|
|
151
|
+
options.onError,
|
|
152
|
+
protocolVersion,
|
|
153
|
+
);
|
|
154
|
+
} finally {
|
|
155
|
+
pending.finish();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
close() {
|
|
159
|
+
closeIndependent();
|
|
160
|
+
return connection.close();
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return server;
|
|
164
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Tool as ProtocolTool } from "@toolgrain/protocol";
|
|
2
|
+
import { type ErrorHandler, reportError } from "../diagnostics.js";
|
|
3
|
+
import type { ToolContext } from "./context.js";
|
|
4
|
+
import { ToolInputError } from "./errors.js";
|
|
5
|
+
import { createRegistry, type ToolMap } from "./registry.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveToolResult,
|
|
8
|
+
type ToolExecutionResult,
|
|
9
|
+
toolError,
|
|
10
|
+
} from "./results.js";
|
|
11
|
+
import { describeTool } from "./schema.js";
|
|
12
|
+
|
|
13
|
+
export interface ToolAdapter {
|
|
14
|
+
has(name: string): boolean;
|
|
15
|
+
list(): ProtocolTool[];
|
|
16
|
+
execute(
|
|
17
|
+
name: string,
|
|
18
|
+
input: unknown,
|
|
19
|
+
ctx: ToolContext,
|
|
20
|
+
): Promise<ToolExecutionResult | undefined>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createToolAdapter(
|
|
24
|
+
tools: ToolMap,
|
|
25
|
+
onError?: ErrorHandler,
|
|
26
|
+
): ToolAdapter {
|
|
27
|
+
const registry = createRegistry(tools);
|
|
28
|
+
const entries = registry.list();
|
|
29
|
+
const names = new Set(entries.map(({ name }) => name));
|
|
30
|
+
const outputs = new Map(entries.map(({ name, output }) => [name, output]));
|
|
31
|
+
const listed = entries.map((entry) => describeTool(entry.name, entry));
|
|
32
|
+
return {
|
|
33
|
+
has: (name: string) => names.has(name),
|
|
34
|
+
list: () => structuredClone(listed),
|
|
35
|
+
async execute(
|
|
36
|
+
name: string,
|
|
37
|
+
input: unknown,
|
|
38
|
+
ctx: ToolContext,
|
|
39
|
+
): Promise<ToolExecutionResult | undefined> {
|
|
40
|
+
const isActive = () => !ctx.signal.aborted;
|
|
41
|
+
if (!names.has(name) || !isActive()) return;
|
|
42
|
+
try {
|
|
43
|
+
const result = await registry.execute(name, input, ctx);
|
|
44
|
+
if (!isActive()) return;
|
|
45
|
+
const converted = await resolveToolResult(
|
|
46
|
+
name,
|
|
47
|
+
result,
|
|
48
|
+
outputs.get(name),
|
|
49
|
+
);
|
|
50
|
+
return isActive() ? converted : undefined;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!isActive()) return;
|
|
53
|
+
try {
|
|
54
|
+
if (error instanceof ToolInputError) {
|
|
55
|
+
return toolError(
|
|
56
|
+
error.issues
|
|
57
|
+
.map(
|
|
58
|
+
(issue) =>
|
|
59
|
+
`${issue.path.map(String).join(".") || "input"}: ${issue.message}`,
|
|
60
|
+
)
|
|
61
|
+
.join("; "),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Inspecting a thrown value can fail, including revoked or trapped proxies.
|
|
66
|
+
}
|
|
67
|
+
reportError(
|
|
68
|
+
onError,
|
|
69
|
+
error,
|
|
70
|
+
Object.freeze({ toolName: name, request: ctx.request }),
|
|
71
|
+
);
|
|
72
|
+
return isActive() ? toolError("Tool execution failed") : undefined;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { INVALID_PARAMS } from "@toolgrain/protocol";
|
|
2
|
+
import { ProtocolError } from "../protocol/responses.js";
|
|
3
|
+
import type { ToolAdapter } from "./adapter.js";
|
|
4
|
+
import type { ToolContext } from "./context.js";
|
|
5
|
+
|
|
6
|
+
/** Execute an already-validated call through the shared registry. */
|
|
7
|
+
export function callTool(
|
|
8
|
+
{
|
|
9
|
+
name,
|
|
10
|
+
arguments: input = {},
|
|
11
|
+
}: { name: string; arguments?: Record<string, unknown> | undefined },
|
|
12
|
+
tools: ToolAdapter,
|
|
13
|
+
ctx: ToolContext,
|
|
14
|
+
) {
|
|
15
|
+
if (!tools.has(name))
|
|
16
|
+
throw new ProtocolError(INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
17
|
+
return tools.execute(name, input, ctx);
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { icon } from "../protocol/icon.js";
|
|
3
|
+
import { object } from "../protocol/messages.js";
|
|
4
|
+
|
|
5
|
+
const annotations = z.looseObject({
|
|
6
|
+
audience: z.array(z.enum(["user", "assistant"])).optional(),
|
|
7
|
+
priority: z.number().min(0).max(1).optional(),
|
|
8
|
+
lastModified: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const common = z.looseObject({
|
|
12
|
+
annotations: annotations.optional(),
|
|
13
|
+
_meta: object.optional(),
|
|
14
|
+
});
|
|
15
|
+
const resourceContents = z.looseObject({
|
|
16
|
+
uri: z.string(),
|
|
17
|
+
mimeType: z.string().optional(),
|
|
18
|
+
_meta: object.optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const contentBlock = z.discriminatedUnion("type", [
|
|
22
|
+
common.extend({ type: z.literal("text"), text: z.string() }),
|
|
23
|
+
common.extend({
|
|
24
|
+
type: z.literal("image"),
|
|
25
|
+
data: z.string(),
|
|
26
|
+
mimeType: z.string(),
|
|
27
|
+
}),
|
|
28
|
+
common.extend({
|
|
29
|
+
type: z.literal("audio"),
|
|
30
|
+
data: z.string(),
|
|
31
|
+
mimeType: z.string(),
|
|
32
|
+
}),
|
|
33
|
+
common.extend({
|
|
34
|
+
type: z.literal("resource_link"),
|
|
35
|
+
uri: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
title: z.string().optional(),
|
|
38
|
+
description: z.string().optional(),
|
|
39
|
+
mimeType: z.string().optional(),
|
|
40
|
+
size: z.number().optional(),
|
|
41
|
+
icons: z.array(icon).optional(),
|
|
42
|
+
}),
|
|
43
|
+
common.extend({
|
|
44
|
+
type: z.literal("resource"),
|
|
45
|
+
resource: z.union([
|
|
46
|
+
resourceContents.extend({ text: z.string() }),
|
|
47
|
+
resourceContents.extend({ blob: z.string() }),
|
|
48
|
+
]),
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export const explicitResult = z.strictObject({
|
|
53
|
+
content: z.array(contentBlock),
|
|
54
|
+
structuredContent: z.unknown().optional(),
|
|
55
|
+
isError: z.boolean().optional(),
|
|
56
|
+
_meta: object.optional(),
|
|
57
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClientCapabilities,
|
|
3
|
+
type Implementation,
|
|
4
|
+
PROTOCOL_VERSION,
|
|
5
|
+
type RequestId,
|
|
6
|
+
type SupportedProtocolVersion,
|
|
7
|
+
} from "@toolgrain/protocol";
|
|
8
|
+
import type {
|
|
9
|
+
ClientCapabilities as LegacyClientCapabilities,
|
|
10
|
+
Implementation as LegacyImplementation,
|
|
11
|
+
} from "@toolgrain/protocol/2025-06-18";
|
|
12
|
+
import { attachAuth } from "../auth/context.js";
|
|
13
|
+
import type { AuthIdentity } from "../auth/types.js";
|
|
14
|
+
import type { RegisteredContextProvider } from "../context/definition.js";
|
|
15
|
+
import {
|
|
16
|
+
type ContextRegistry,
|
|
17
|
+
createContextRegistry,
|
|
18
|
+
} from "../context/registry.js";
|
|
19
|
+
import { createContextScope } from "../context/scope.js";
|
|
20
|
+
import type { DeepReadonly } from "./json-value.js";
|
|
21
|
+
|
|
22
|
+
/** What the client reported about itself; unverified, so not a basis for security decisions. */
|
|
23
|
+
export interface ToolClient {
|
|
24
|
+
readonly info?: DeepReadonly<Implementation | LegacyImplementation>;
|
|
25
|
+
readonly capabilities?: DeepReadonly<
|
|
26
|
+
ClientCapabilities | LegacyClientCapabilities
|
|
27
|
+
>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ToolRequest {
|
|
31
|
+
readonly id: RequestId;
|
|
32
|
+
readonly protocolVersion: SupportedProtocolVersion;
|
|
33
|
+
/** Absent when the era and transport carry no client information, such as legacy HTTP. */
|
|
34
|
+
readonly client?: ToolClient;
|
|
35
|
+
/** A deeply frozen copy of the request's `_meta`; empty when the request carried none. */
|
|
36
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ToolContext {
|
|
40
|
+
/** Aborts when the client cancels or disconnects, or the transport or server closes. */
|
|
41
|
+
readonly signal: AbortSignal;
|
|
42
|
+
readonly request: ToolRequest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ToolContextOptions {
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
request?: Partial<ToolRequest>;
|
|
48
|
+
context?: readonly RegisteredContextProvider[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Build a context for calling a definition's `execute` directly, for example in tests. */
|
|
52
|
+
export function toolContext(options: ToolContextOptions = {}): ToolContext {
|
|
53
|
+
// Default each field separately so an explicit undefined cannot replace a default.
|
|
54
|
+
const {
|
|
55
|
+
id = 0,
|
|
56
|
+
protocolVersion = PROTOCOL_VERSION,
|
|
57
|
+
client,
|
|
58
|
+
meta = {},
|
|
59
|
+
} = options.request ?? {};
|
|
60
|
+
return createToolContext(
|
|
61
|
+
options.signal ?? new AbortController().signal,
|
|
62
|
+
{
|
|
63
|
+
id,
|
|
64
|
+
protocolVersion,
|
|
65
|
+
...(client === undefined ? {} : { client }),
|
|
66
|
+
meta,
|
|
67
|
+
},
|
|
68
|
+
undefined,
|
|
69
|
+
createContextRegistry(options.context),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Each request gets its own deeply frozen copies, so handlers cannot affect one another. */
|
|
74
|
+
export function createToolContext(
|
|
75
|
+
signal: AbortSignal,
|
|
76
|
+
{ client, ...request }: ToolRequest,
|
|
77
|
+
auth?: AuthIdentity,
|
|
78
|
+
providers: ContextRegistry = createContextRegistry(),
|
|
79
|
+
): ToolContext {
|
|
80
|
+
const ctx = attachAuth(
|
|
81
|
+
{
|
|
82
|
+
signal,
|
|
83
|
+
request: Object.freeze({
|
|
84
|
+
...request,
|
|
85
|
+
...(client === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: {
|
|
88
|
+
client: frozenCopy(toolClient(client.info, client.capabilities)),
|
|
89
|
+
}),
|
|
90
|
+
meta: frozenCopy(request.meta),
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
auth,
|
|
94
|
+
);
|
|
95
|
+
return createContextScope(ctx, providers);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Omit absent fields so the client matches its optional-property types. */
|
|
99
|
+
export function toolClient(
|
|
100
|
+
info: ToolClient["info"],
|
|
101
|
+
capabilities: ToolClient["capabilities"],
|
|
102
|
+
): ToolClient {
|
|
103
|
+
return {
|
|
104
|
+
...(info === undefined ? {} : { info }),
|
|
105
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function frozenCopy<T>(value: T): T {
|
|
110
|
+
return deepFreeze(structuredClone(value));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function deepFreeze<T>(value: T): T {
|
|
114
|
+
if (typeof value === "object" && value !== null) {
|
|
115
|
+
Object.freeze(value);
|
|
116
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|