@toolgrain/mcp 0.3.0-alpha.0 → 0.3.0-alpha.2
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/CHANGELOG.md +19 -0
- package/README.md +17 -3
- package/dist/apps.d.ts +9 -0
- package/dist/apps.d.ts.map +1 -0
- package/dist/apps.js +21 -0
- package/dist/apps.js.map +1 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +9 -1
- package/dist/connection.js.map +1 -1
- package/dist/dispatcher.d.ts +2 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +3 -3
- package/dist/dispatcher.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/legacy/dispatcher.d.ts +2 -1
- package/dist/legacy/dispatcher.d.ts.map +1 -1
- package/dist/legacy/dispatcher.js +23 -8
- package/dist/legacy/dispatcher.js.map +1 -1
- package/dist/legacy/handlers.d.ts +1 -1
- package/dist/legacy/handlers.d.ts.map +1 -1
- package/dist/legacy/handlers.js +2 -2
- package/dist/legacy/handlers.js.map +1 -1
- package/dist/modern/dispatcher.d.ts +2 -1
- package/dist/modern/dispatcher.d.ts.map +1 -1
- package/dist/modern/dispatcher.js +22 -8
- package/dist/modern/dispatcher.js.map +1 -1
- package/dist/modern/handlers.d.ts +1 -1
- package/dist/modern/handlers.d.ts.map +1 -1
- package/dist/modern/handlers.js +2 -2
- package/dist/modern/handlers.js.map +1 -1
- package/dist/modern/results.d.ts +6 -0
- package/dist/modern/results.d.ts.map +1 -1
- package/dist/modern/results.js +9 -0
- package/dist/modern/results.js.map +1 -1
- package/dist/pending-requests.d.ts +2 -1
- package/dist/pending-requests.d.ts.map +1 -1
- package/dist/pending-requests.js +17 -0
- package/dist/pending-requests.js.map +1 -1
- package/dist/resources/definition.d.ts +45 -0
- package/dist/resources/definition.d.ts.map +1 -0
- package/dist/resources/definition.js +46 -0
- package/dist/resources/definition.js.map +1 -0
- package/dist/resources/registry.d.ts +13 -0
- package/dist/resources/registry.d.ts.map +1 -0
- package/dist/resources/registry.js +48 -0
- package/dist/resources/registry.js.map +1 -0
- package/dist/resources/requests.d.ts +13 -0
- package/dist/resources/requests.d.ts.map +1 -0
- package/dist/resources/requests.js +43 -0
- package/dist/resources/requests.js.map +1 -0
- package/dist/resources/validation.d.ts +33 -0
- package/dist/resources/validation.d.ts.map +1 -0
- package/dist/resources/validation.js +61 -0
- package/dist/resources/validation.js.map +1 -0
- package/dist/server.d.ts +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -3
- package/dist/server.js.map +1 -1
- package/dist/tools/context.d.ts +5 -4
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +1 -1
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/progress.d.ts +4 -2
- package/dist/tools/progress.d.ts.map +1 -1
- package/dist/tools/progress.js +40 -6
- package/dist/tools/progress.js.map +1 -1
- package/dist/tools/rich-result.d.ts +19 -19
- package/dist/tools/rich-result.d.ts.map +1 -1
- package/dist/tools/rich-result.js.map +1 -1
- package/package.json +2 -2
- package/src/apps.ts +23 -0
- package/src/connection.ts +15 -0
- package/src/dispatcher.ts +4 -0
- package/src/index.ts +9 -0
- package/src/legacy/dispatcher.ts +55 -13
- package/src/legacy/handlers.ts +2 -1
- package/src/modern/dispatcher.ts +57 -21
- package/src/modern/handlers.ts +2 -1
- package/src/modern/results.ts +10 -0
- package/src/pending-requests.ts +17 -1
- package/src/resources/definition.ts +92 -0
- package/src/resources/registry.ts +72 -0
- package/src/resources/requests.ts +59 -0
- package/src/resources/validation.ts +82 -0
- package/src/server.ts +11 -4
- package/src/tools/context.ts +5 -4
- package/src/tools/progress.ts +46 -8
- package/src/tools/rich-result.ts +53 -27
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlobResourceContents,
|
|
3
|
+
MetaObject,
|
|
4
|
+
Resource as ProtocolResource,
|
|
5
|
+
TextResourceContents,
|
|
6
|
+
} from "@toolgrain/protocol";
|
|
7
|
+
import type { ToolContext } from "../tools/context.js";
|
|
8
|
+
import type { DeepReadonly } from "../tools/json-value.js";
|
|
9
|
+
|
|
10
|
+
/** Readonly resource identity and display metadata, copied during server registration. */
|
|
11
|
+
export type ResourceMetadata = DeepReadonly<ProtocolResource>;
|
|
12
|
+
|
|
13
|
+
/** Explicit resource contents; each item supplies its own URI and exactly one payload. */
|
|
14
|
+
export interface ResourceReadResult {
|
|
15
|
+
/** Text or base64 binary entries; an existing resource may return an empty array. */
|
|
16
|
+
readonly contents: readonly (
|
|
17
|
+
| (DeepReadonly<TextResourceContents> & { readonly blob?: never })
|
|
18
|
+
| (DeepReadonly<BlobResourceContents> & { readonly text?: never })
|
|
19
|
+
)[];
|
|
20
|
+
/** Optional result metadata, separate from each content item's metadata. */
|
|
21
|
+
readonly _meta?: DeepReadonly<MetaObject>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ReadResult = ResourceReadResult | PromiseLike<ResourceReadResult>;
|
|
25
|
+
|
|
26
|
+
/** A fixed resource and its application-owned reader. */
|
|
27
|
+
export interface ResourceDefinition<Result extends ReadResult = ReadResult>
|
|
28
|
+
extends ResourceMetadata {
|
|
29
|
+
/**
|
|
30
|
+
* Read this URI using the request's auth, providers, and cancellation signal.
|
|
31
|
+
* Direct calls bypass server authorization and result validation.
|
|
32
|
+
*/
|
|
33
|
+
readonly read: (uri: string, ctx: ToolContext) => Result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const RESOURCE_METADATA_KEYS = [
|
|
37
|
+
"uri",
|
|
38
|
+
"name",
|
|
39
|
+
"title",
|
|
40
|
+
"description",
|
|
41
|
+
"mimeType",
|
|
42
|
+
"annotations",
|
|
43
|
+
"size",
|
|
44
|
+
"icons",
|
|
45
|
+
"_meta",
|
|
46
|
+
] as const;
|
|
47
|
+
const brand = Symbol.for("@toolgrain/mcp/resource@1");
|
|
48
|
+
|
|
49
|
+
/** Frozen reusable definition returned by `resource()`; direct reads remain raw. */
|
|
50
|
+
export type Resource<Result extends ReadResult = ReadResult> =
|
|
51
|
+
ResourceDefinition<Result> & ResourceValue;
|
|
52
|
+
|
|
53
|
+
class ResourceValue {
|
|
54
|
+
constructor(definition: ResourceDefinition) {
|
|
55
|
+
for (const key of [...RESOURCE_METADATA_KEYS, "read"] as const) {
|
|
56
|
+
const value = definition[key];
|
|
57
|
+
if (value !== undefined)
|
|
58
|
+
Object.defineProperty(this, key, { value, enumerable: true });
|
|
59
|
+
}
|
|
60
|
+
Object.freeze(this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Structural brand shared by compatible package copies; object spreads drop it. */
|
|
64
|
+
"~toolgrain"(): "resource@1" {
|
|
65
|
+
return "resource@1";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Object.defineProperty(ResourceValue.prototype, brand, { value: true });
|
|
69
|
+
|
|
70
|
+
export function isResource(value: unknown): value is Resource {
|
|
71
|
+
return (
|
|
72
|
+
typeof value === "object" &&
|
|
73
|
+
value !== null &&
|
|
74
|
+
(value as { [brand]?: unknown })[brand] === true
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Declare a reusable fixed-URI resource without reading its contents.
|
|
80
|
+
* Registration validates and snapshots metadata; server reads validate and copy results.
|
|
81
|
+
* No URI is fetched or mapped to a filesystem automatically.
|
|
82
|
+
* @param definition - Absolute URI, display metadata, and synchronous or async reader.
|
|
83
|
+
* @returns A frozen definition preserving the reader's inferred return type.
|
|
84
|
+
* @throws `TypeError` if the reader is not a function.
|
|
85
|
+
*/
|
|
86
|
+
export function resource<Result extends ReadResult>(
|
|
87
|
+
definition: ResourceDefinition<Result>,
|
|
88
|
+
): Resource<Result> {
|
|
89
|
+
if (typeof definition?.read !== "function")
|
|
90
|
+
throw new TypeError("Resources require a read function");
|
|
91
|
+
return new ResourceValue(definition) as Resource<Result>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTERNAL_ERROR,
|
|
3
|
+
type Resource as ProtocolResource,
|
|
4
|
+
} from "@toolgrain/protocol";
|
|
5
|
+
import { type ErrorHandler, reportError } from "../diagnostics.js";
|
|
6
|
+
import { ProtocolError } from "../protocol/responses.js";
|
|
7
|
+
import type { ToolContext } from "../tools/context.js";
|
|
8
|
+
import { assertJSON } from "../tools/json-value.js";
|
|
9
|
+
import {
|
|
10
|
+
isResource,
|
|
11
|
+
RESOURCE_METADATA_KEYS,
|
|
12
|
+
type Resource,
|
|
13
|
+
} from "./definition.js";
|
|
14
|
+
import {
|
|
15
|
+
copyReadResult,
|
|
16
|
+
type ResourceExecutionResult,
|
|
17
|
+
resourceMetadata,
|
|
18
|
+
} from "./validation.js";
|
|
19
|
+
|
|
20
|
+
type Reader = (
|
|
21
|
+
ctx: ToolContext,
|
|
22
|
+
) => Promise<ResourceExecutionResult | undefined>;
|
|
23
|
+
export interface ResourceRegistry {
|
|
24
|
+
list(): ProtocolResource[];
|
|
25
|
+
reader(uri: string): Reader | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createResourceRegistry(
|
|
29
|
+
definitions: readonly Resource[],
|
|
30
|
+
onError?: ErrorHandler,
|
|
31
|
+
): ResourceRegistry {
|
|
32
|
+
const entries = new Map<
|
|
33
|
+
string,
|
|
34
|
+
{ metadata: ProtocolResource; read: Reader }
|
|
35
|
+
>();
|
|
36
|
+
for (const definition of definitions) {
|
|
37
|
+
if (!isResource(definition))
|
|
38
|
+
throw new TypeError("Resources must be created with resource()");
|
|
39
|
+
const fields: Record<string, unknown> = {};
|
|
40
|
+
for (const key of RESOURCE_METADATA_KEYS)
|
|
41
|
+
if (definition[key] !== undefined) fields[key] = definition[key];
|
|
42
|
+
assertJSON(fields);
|
|
43
|
+
const metadata = resourceMetadata.parse(
|
|
44
|
+
structuredClone(fields),
|
|
45
|
+
) as ProtocolResource;
|
|
46
|
+
if (entries.has(metadata.uri))
|
|
47
|
+
throw new TypeError(`Duplicate resource URI: ${metadata.uri}`);
|
|
48
|
+
if (typeof definition.read !== "function")
|
|
49
|
+
throw new TypeError("Resources require a read function");
|
|
50
|
+
entries.set(metadata.uri, {
|
|
51
|
+
metadata,
|
|
52
|
+
async read(ctx) {
|
|
53
|
+
if (ctx.signal.aborted) return;
|
|
54
|
+
try {
|
|
55
|
+
const value = await definition.read(metadata.uri, ctx);
|
|
56
|
+
if (ctx.signal.aborted) return;
|
|
57
|
+
const result = copyReadResult(value);
|
|
58
|
+
return ctx.signal.aborted ? undefined : result;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (ctx.signal.aborted) return;
|
|
61
|
+
reportError(onError, error);
|
|
62
|
+
throw new ProtocolError(INTERNAL_ERROR, "Internal error");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
list: () =>
|
|
69
|
+
structuredClone(Array.from(entries.values(), ({ metadata }) => metadata)),
|
|
70
|
+
reader: (uri) => entries.get(uri)?.read,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { INVALID_PARAMS, METHOD_NOT_FOUND } from "@toolgrain/protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { object } from "../protocol/messages.js";
|
|
4
|
+
import { ProtocolError } from "../protocol/responses.js";
|
|
5
|
+
import type { ToolContext } from "../tools/context.js";
|
|
6
|
+
import type { ResourceRegistry } from "./registry.js";
|
|
7
|
+
import { resourceUri } from "./validation.js";
|
|
8
|
+
|
|
9
|
+
const listParams = z.looseObject({
|
|
10
|
+
_meta: object.optional(),
|
|
11
|
+
cursor: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
const readParams = z.looseObject({
|
|
14
|
+
_meta: object.optional(),
|
|
15
|
+
uri: resourceUri,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function requireResources(
|
|
19
|
+
resources: ResourceRegistry | undefined,
|
|
20
|
+
): ResourceRegistry {
|
|
21
|
+
if (!resources) throw new ProtocolError(METHOD_NOT_FOUND, "Method not found");
|
|
22
|
+
return resources;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateList(params: unknown) {
|
|
26
|
+
const parsed = listParams.safeParse(params ?? {});
|
|
27
|
+
if (!parsed.success || parsed.data.cursor !== undefined)
|
|
28
|
+
throw new ProtocolError(INVALID_PARAMS, "Invalid resource list parameters");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function listResources(params: unknown, resources: ResourceRegistry) {
|
|
32
|
+
validateList(params);
|
|
33
|
+
return { resources: resources.list() };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listResourceTemplates(
|
|
37
|
+
params: unknown,
|
|
38
|
+
_resources: ResourceRegistry,
|
|
39
|
+
) {
|
|
40
|
+
validateList(params);
|
|
41
|
+
return { resourceTemplates: [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readResource(
|
|
45
|
+
params: unknown,
|
|
46
|
+
resources: ResourceRegistry,
|
|
47
|
+
ctx: (params: { _meta?: Record<string, unknown> | undefined }) => ToolContext,
|
|
48
|
+
notFoundCode: number,
|
|
49
|
+
) {
|
|
50
|
+
const parsed = readParams.safeParse(params);
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
throw new ProtocolError(INVALID_PARAMS, "Invalid resource read parameters");
|
|
53
|
+
const read = resources.reader(parsed.data.uri);
|
|
54
|
+
if (!read)
|
|
55
|
+
throw new ProtocolError(notFoundCode, "Resource not found", {
|
|
56
|
+
uri: parsed.data.uri,
|
|
57
|
+
});
|
|
58
|
+
return read(ctx(parsed.data));
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ReadResourceResult } from "@toolgrain/protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { icon } from "../protocol/icon.js";
|
|
4
|
+
import { object } from "../protocol/messages.js";
|
|
5
|
+
import { assertJSON } from "../tools/json-value.js";
|
|
6
|
+
export type ResourceExecutionResult = Pick<
|
|
7
|
+
ReadResourceResult,
|
|
8
|
+
"contents" | "_meta"
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
// RFC 3986 components: percent-encoded bytes, unreserved characters, and sub-delimiters.
|
|
12
|
+
const atom = "(?:[A-Za-z0-9._~-]|%[A-Fa-f0-9]{2}|[!$&'()*+,;=])";
|
|
13
|
+
const path = new RegExp(`^(?:${atom}|[:@/])*$`);
|
|
14
|
+
const queryOrFragment = new RegExp(`^(?:${atom}|[:@/?])*$`);
|
|
15
|
+
const authority = new RegExp(
|
|
16
|
+
`^(?:(?:${atom}|:)*@)?(?:${atom}*|\\[([^\\]]+)\\])(?::[0-9]*)?$`,
|
|
17
|
+
);
|
|
18
|
+
const ipFuture = /^v[A-Fa-f0-9]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$/i;
|
|
19
|
+
|
|
20
|
+
/** Validate URI components without normalizing registry identities or treating them as URLs. */
|
|
21
|
+
export const resourceUri = z.string().refine((value) => {
|
|
22
|
+
const parts =
|
|
23
|
+
/^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#([^#]*))?$/.exec(
|
|
24
|
+
value,
|
|
25
|
+
);
|
|
26
|
+
if (
|
|
27
|
+
!parts ||
|
|
28
|
+
!path.test(parts[2] ?? "") ||
|
|
29
|
+
!queryOrFragment.test(parts[3] ?? "") ||
|
|
30
|
+
!queryOrFragment.test(parts[4] ?? "")
|
|
31
|
+
)
|
|
32
|
+
return false;
|
|
33
|
+
if (parts[1] === undefined) return true;
|
|
34
|
+
const host = authority.exec(parts[1]);
|
|
35
|
+
if (!host) return false;
|
|
36
|
+
const ip = host[1];
|
|
37
|
+
return (
|
|
38
|
+
ip === undefined || ipFuture.test(ip) || URL.canParse(`https://[${ip}]/`)
|
|
39
|
+
);
|
|
40
|
+
}, "Resource URI must be an absolute URI");
|
|
41
|
+
|
|
42
|
+
export const resourceMetadata = z.strictObject({
|
|
43
|
+
uri: resourceUri,
|
|
44
|
+
name: z.string().min(1),
|
|
45
|
+
title: z.string().optional(),
|
|
46
|
+
description: z.string().optional(),
|
|
47
|
+
mimeType: z.string().optional(),
|
|
48
|
+
annotations: z
|
|
49
|
+
.looseObject({
|
|
50
|
+
audience: z.array(z.enum(["user", "assistant"])).optional(),
|
|
51
|
+
priority: z.number().min(0).max(1).optional(),
|
|
52
|
+
lastModified: z.string().optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
55
|
+
size: z.number().int().nonnegative().optional(),
|
|
56
|
+
icons: z.array(icon).optional(),
|
|
57
|
+
_meta: object.optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const contents = z
|
|
61
|
+
.looseObject({
|
|
62
|
+
uri: resourceUri,
|
|
63
|
+
mimeType: z.string().optional(),
|
|
64
|
+
text: z.string().optional(),
|
|
65
|
+
blob: z.base64().optional(),
|
|
66
|
+
_meta: object.optional(),
|
|
67
|
+
})
|
|
68
|
+
.refine(
|
|
69
|
+
(value) => Object.hasOwn(value, "text") !== Object.hasOwn(value, "blob"),
|
|
70
|
+
"Resource contents require exactly one of text or blob",
|
|
71
|
+
);
|
|
72
|
+
const readResult = z.strictObject({
|
|
73
|
+
contents: z.array(contents),
|
|
74
|
+
_meta: object.optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export function copyReadResult(value: unknown): ResourceExecutionResult {
|
|
78
|
+
assertJSON(value, { omitUndefined: true });
|
|
79
|
+
return readResult.parse(
|
|
80
|
+
JSON.parse(JSON.stringify(value)),
|
|
81
|
+
) as ResourceExecutionResult;
|
|
82
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { type ErrorHandler, reportError } from "./diagnostics.js";
|
|
|
15
15
|
import { createDispatcher, formatErrorResponse } from "./dispatcher.js";
|
|
16
16
|
import { executeRequest } from "./execute-request.js";
|
|
17
17
|
import { decode } from "./protocol/decode.js";
|
|
18
|
+
import type { Resource } from "./resources/definition.js";
|
|
19
|
+
import { createResourceRegistry } from "./resources/registry.js";
|
|
18
20
|
import { createToolAdapter } from "./tools/adapter.js";
|
|
19
21
|
import type { ToolMap } from "./tools/registry.js";
|
|
20
22
|
import {
|
|
@@ -30,7 +32,7 @@ interface ResponseDeliveryOptions {
|
|
|
30
32
|
[responseDelivery]?: (response: ProtocolResponse) => void | Promise<void>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/** Tool registrations, identity, and shared configuration for an MCP server. */
|
|
35
|
+
/** Tool/resource registrations, identity, and shared configuration for an MCP server. */
|
|
34
36
|
export interface ServerOptions<Tools extends ToolMap = ToolMap> {
|
|
35
37
|
/** Server implementation name advertised to clients. */
|
|
36
38
|
name: string;
|
|
@@ -38,12 +40,14 @@ export interface ServerOptions<Tools extends ToolMap = ToolMap> {
|
|
|
38
40
|
version: string;
|
|
39
41
|
/** Tools made with `tool()`; each registration key becomes its exposed tool name. */
|
|
40
42
|
tools: Tools;
|
|
43
|
+
/** Fixed resources made with `resource()`; supplying an empty array enables an empty resource service. */
|
|
44
|
+
resources?: readonly Resource[];
|
|
41
45
|
/** Optional usage instructions advertised to clients during discovery or initialization. */
|
|
42
46
|
instructions?: string;
|
|
43
47
|
/**
|
|
44
48
|
* Receives original unexpected failures for private diagnostics. Tool failures
|
|
45
49
|
* include frozen tool/request details; auth and transport failures pass only
|
|
46
|
-
* the error. Throws and rejected promises from this callback are ignored.
|
|
50
|
+
* the error, as do resource failures. Throws and rejected promises from this callback are ignored.
|
|
47
51
|
*/
|
|
48
52
|
onError?: ErrorHandler;
|
|
49
53
|
/** Eager endpoint authorization; requires a request transport rather than stdio. */
|
|
@@ -122,8 +126,8 @@ export interface Server {
|
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
/**
|
|
125
|
-
* Create a server from reusable tools, optional auth, and lazy context providers.
|
|
126
|
-
* Registration validates tool names,
|
|
129
|
+
* Create a server from reusable tools/resources, optional auth, and lazy context providers.
|
|
130
|
+
* Registration validates tool names/schemas, resource URIs, and metadata immediately.
|
|
127
131
|
* Creating the server does not start a transport or resolve context providers.
|
|
128
132
|
*
|
|
129
133
|
* @param options - Server identity and application-owned tool/provider registrations.
|
|
@@ -140,6 +144,9 @@ export function createServer<const Tools extends ToolMap>(
|
|
|
140
144
|
{ name: options.name, version: options.version },
|
|
141
145
|
options.instructions,
|
|
142
146
|
createContextRegistry(options.context),
|
|
147
|
+
options.resources === undefined
|
|
148
|
+
? undefined
|
|
149
|
+
: createResourceRegistry(options.resources, options.onError),
|
|
143
150
|
);
|
|
144
151
|
let closed = false;
|
|
145
152
|
const running = new Set<AbortController>();
|
package/src/tools/context.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface ToolRequest {
|
|
|
42
42
|
readonly meta: Readonly<Record<string, unknown>>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** Per-invocation data passed to tool handlers and lazy context resolvers. */
|
|
45
|
+
/** Per-invocation data passed to tool handlers, resource readers, and lazy context resolvers. */
|
|
46
46
|
export interface ToolContext {
|
|
47
47
|
/** Aborts when the client cancels or disconnects, or the transport or server closes. */
|
|
48
48
|
readonly signal: AbortSignal;
|
|
@@ -50,8 +50,9 @@ export interface ToolContext {
|
|
|
50
50
|
readonly request: ToolRequest;
|
|
51
51
|
/**
|
|
52
52
|
* Report request-scoped progress; safe to pass to helpers without binding.
|
|
53
|
-
* Await delivery/backpressure
|
|
54
|
-
* or delivery channel exists,
|
|
53
|
+
* Await delivery/backpressure; updates are spaced at least 100 ms per invocation.
|
|
54
|
+
* Resolves without sending when no progress token or delivery channel exists,
|
|
55
|
+
* or after cancellation/completion. Active updates
|
|
55
56
|
* require finite values and strictly increasing progress; invalid updates reject.
|
|
56
57
|
* Delivery failure cancels the request and reaches transport diagnostics.
|
|
57
58
|
*/
|
|
@@ -69,7 +70,7 @@ export interface ToolContextOptions {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* Build an unauthenticated context for direct `execute()`
|
|
73
|
+
* Build an unauthenticated context for direct tool `execute()` or resource `read()` calls.
|
|
73
74
|
* Request metadata is copied and deeply frozen; each call creates a fresh provider cache.
|
|
74
75
|
* Direct execution still bypasses server authorization, validation, and MCP conversion.
|
|
75
76
|
*
|
package/src/tools/progress.ts
CHANGED
|
@@ -18,20 +18,45 @@ const update = z.object({
|
|
|
18
18
|
message: z.string().optional(),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
/** Tokens share the request-ID wire format, but have a separate lifetime. */
|
|
22
|
+
export function isProgressToken(token: unknown): token is ProgressToken {
|
|
23
|
+
return (
|
|
24
|
+
typeof token === "string" ||
|
|
25
|
+
(typeof token === "number" &&
|
|
26
|
+
Number.isFinite(token) &&
|
|
27
|
+
Number.isInteger(token))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Cancellation must release the timer as well as the awaiting handler. */
|
|
32
|
+
function waitForProgress(milliseconds: number, signal: AbortSignal) {
|
|
33
|
+
return new Promise<void>((resolve, reject) => {
|
|
34
|
+
const abort = () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(signal.reason);
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
signal.removeEventListener("abort", abort);
|
|
40
|
+
resolve();
|
|
41
|
+
}, milliseconds);
|
|
42
|
+
if (signal.aborted) abort();
|
|
43
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
/** One invocation owns its reporter, ordering, and completion boundary. */
|
|
22
48
|
export function createProgressReporter(
|
|
23
|
-
|
|
49
|
+
requestSignal: AbortSignal,
|
|
24
50
|
token: unknown,
|
|
25
51
|
send?: (message: ProgressNotification) => void | Promise<void>,
|
|
26
52
|
) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Number.isFinite(token) &&
|
|
31
|
-
Number.isInteger(token));
|
|
53
|
+
const lifetime = new AbortController();
|
|
54
|
+
const signal = AbortSignal.any([requestSignal, lifetime.signal]);
|
|
55
|
+
const validToken = isProgressToken(token);
|
|
32
56
|
let open = true;
|
|
33
57
|
let previous = Number.NEGATIVE_INFINITY;
|
|
34
58
|
let writes = Promise.resolve();
|
|
59
|
+
let lastSent = Number.NEGATIVE_INFINITY;
|
|
35
60
|
const report = (value: ToolProgress): Promise<void> => {
|
|
36
61
|
if (!open || signal.aborted || !validToken || !send)
|
|
37
62
|
return Promise.resolve();
|
|
@@ -56,13 +81,23 @@ export function createProgressReporter(
|
|
|
56
81
|
},
|
|
57
82
|
};
|
|
58
83
|
const write = writes.then(async () => {
|
|
59
|
-
if (!signal.aborted)
|
|
84
|
+
if (!signal.aborted) {
|
|
85
|
+
// Pace each operation without dropping updates or bypassing backpressure.
|
|
86
|
+
let remaining = 100 - (performance.now() - lastSent);
|
|
87
|
+
while (remaining > 0) {
|
|
88
|
+
await waitForProgress(Math.ceil(remaining), signal);
|
|
89
|
+
remaining = 100 - (performance.now() - lastSent);
|
|
90
|
+
}
|
|
60
91
|
await abortable(
|
|
61
92
|
Promise.resolve().then(() => {
|
|
62
|
-
if (!signal.aborted)
|
|
93
|
+
if (!signal.aborted) {
|
|
94
|
+
lastSent = performance.now();
|
|
95
|
+
return send(message);
|
|
96
|
+
}
|
|
63
97
|
}),
|
|
64
98
|
signal,
|
|
65
99
|
);
|
|
100
|
+
}
|
|
66
101
|
});
|
|
67
102
|
// The awaiting handler observes failures; keep the queue usable for cleanup.
|
|
68
103
|
writes = write.catch(() => {});
|
|
@@ -76,6 +111,9 @@ export function createProgressReporter(
|
|
|
76
111
|
},
|
|
77
112
|
close() {
|
|
78
113
|
open = false;
|
|
114
|
+
lifetime.abort(
|
|
115
|
+
new DOMException("Progress reporting closed", "AbortError"),
|
|
116
|
+
);
|
|
79
117
|
},
|
|
80
118
|
};
|
|
81
119
|
}
|
package/src/tools/rich-result.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type { ContentBlock, MetaObject } from "@toolgrain/protocol";
|
|
|
2
2
|
import type { DeepReadonly, JsonCompatible } from "./json-value.js";
|
|
3
3
|
|
|
4
4
|
/** Explicit MCP content and optional structured data, wrapped with `mcpResult()`. */
|
|
5
|
-
export interface McpResultOptions<
|
|
5
|
+
export interface McpResultOptions<
|
|
6
|
+
Structured = unknown,
|
|
7
|
+
Meta extends MetaObject = MetaObject,
|
|
8
|
+
> {
|
|
6
9
|
/** Content blocks for the client; readonly authoring arrays are accepted. */
|
|
7
10
|
content: readonly DeepReadonly<ContentBlock>[];
|
|
8
11
|
/** JSON-compatible data; required for successful rich results with a declared output schema. */
|
|
@@ -10,7 +13,7 @@ export interface McpResultOptions<Structured = unknown> {
|
|
|
10
13
|
/** Marks an expected, client-visible tool failure; omitted or false indicates success. */
|
|
11
14
|
isError?: boolean;
|
|
12
15
|
/** Optional protocol result metadata, distinct from application structured data. */
|
|
13
|
-
_meta?:
|
|
16
|
+
_meta?: Meta;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
// Registered symbols let duplicate package copies recognize each other's results.
|
|
@@ -20,10 +23,10 @@ const RESULT_BRAND = Symbol.for("@toolgrain/mcp/result@1");
|
|
|
20
23
|
* Branded rich-result wrapper recognized by compatible Toolgrain package copies.
|
|
21
24
|
* Use `mcpResult()` to construct one; a plain object with the same fields remains application data.
|
|
22
25
|
*/
|
|
23
|
-
export class McpResult<Structured> {
|
|
26
|
+
export class McpResult<Structured, Meta extends MetaObject = MetaObject> {
|
|
24
27
|
constructor(
|
|
25
28
|
/** Explicit MCP result envelope, before server validation and revision-specific conversion. */
|
|
26
|
-
readonly value: McpResultOptions<Structured>,
|
|
29
|
+
readonly value: McpResultOptions<Structured, Meta>,
|
|
27
30
|
) {
|
|
28
31
|
Object.freeze(this);
|
|
29
32
|
}
|
|
@@ -45,18 +48,24 @@ export function isMcpResult(value: unknown): value is McpResult<unknown> {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/** Explicit, client-visible tool error; structured data may be omitted for text-only errors. */
|
|
48
|
-
export type McpErrorResult<
|
|
51
|
+
export type McpErrorResult<
|
|
52
|
+
Structured = never,
|
|
53
|
+
Meta extends MetaObject = MetaObject,
|
|
54
|
+
> = McpResult<Structured, Meta> & {
|
|
49
55
|
/** Envelope statically marked as an error. */
|
|
50
|
-
readonly value: McpResultOptions<Structured> & {
|
|
56
|
+
readonly value: McpResultOptions<Structured, Meta> & {
|
|
51
57
|
/** Explicit expected-failure marker, preserved in the MCP result. */
|
|
52
58
|
isError: true;
|
|
53
59
|
};
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/** A result whose structured data is present, as required by an output schema. */
|
|
57
|
-
export type McpStructuredResult<
|
|
63
|
+
export type McpStructuredResult<
|
|
64
|
+
Structured,
|
|
65
|
+
Meta extends MetaObject = MetaObject,
|
|
66
|
+
> = McpResult<Structured, Meta> & {
|
|
58
67
|
/** Envelope with required application structured data. */
|
|
59
|
-
readonly value: McpResultOptions<Structured> & {
|
|
68
|
+
readonly value: McpResultOptions<Structured, Meta> & {
|
|
60
69
|
/** Structured value validated against the tool's output schema when one is declared. */
|
|
61
70
|
structuredContent: Structured;
|
|
62
71
|
};
|
|
@@ -68,53 +77,68 @@ export type McpStructuredResult<Structured> = McpResult<Structured> & {
|
|
|
68
77
|
* @param value - Error content and JSON-compatible data, parsed by any declared output schema.
|
|
69
78
|
* @returns A branded result typed as both an error and a structured result.
|
|
70
79
|
*/
|
|
71
|
-
export function mcpResult<
|
|
72
|
-
|
|
80
|
+
export function mcpResult<
|
|
81
|
+
Structured = never,
|
|
82
|
+
const Meta extends MetaObject = MetaObject,
|
|
83
|
+
>(
|
|
84
|
+
value: McpResultOptions<Structured, Meta> & {
|
|
73
85
|
/** Mark this result as an expected, client-visible tool failure. */
|
|
74
86
|
isError: true;
|
|
75
87
|
/** Structured error data, subject to output-schema validation when declared. */
|
|
76
88
|
structuredContent: Structured;
|
|
77
89
|
},
|
|
78
|
-
): McpErrorResult<Structured> & McpStructuredResult<Structured>;
|
|
90
|
+
): McpErrorResult<Structured, Meta> & McpStructuredResult<Structured, Meta>;
|
|
79
91
|
/**
|
|
80
92
|
* Wrap a text/content-only public tool error, allowed even when the tool declares output.
|
|
81
93
|
* @param value - Explicit error content without structured application data.
|
|
82
94
|
* @returns A branded error result whose `isError` flag is statically true.
|
|
83
95
|
*/
|
|
84
|
-
export function mcpResult
|
|
85
|
-
|
|
96
|
+
export function mcpResult<
|
|
97
|
+
Structured = never,
|
|
98
|
+
const Meta extends MetaObject = MetaObject,
|
|
99
|
+
>(
|
|
100
|
+
value: McpResultOptions<never, Meta> & {
|
|
86
101
|
/** Mark this result as an expected, client-visible tool failure. */
|
|
87
102
|
isError: true;
|
|
88
103
|
},
|
|
89
|
-
): McpErrorResult<
|
|
104
|
+
): McpErrorResult<Structured, Meta>;
|
|
90
105
|
/**
|
|
91
106
|
* Wrap a public tool error with an optional structured-data contract.
|
|
92
107
|
* @param value - Error content and any JSON-compatible structured data.
|
|
93
108
|
* @returns A branded error result; provided structured data still undergoes output validation.
|
|
94
109
|
*/
|
|
95
|
-
export function mcpResult<
|
|
96
|
-
|
|
110
|
+
export function mcpResult<
|
|
111
|
+
Structured,
|
|
112
|
+
const Meta extends MetaObject = MetaObject,
|
|
113
|
+
>(
|
|
114
|
+
value: McpResultOptions<Structured, Meta> & {
|
|
97
115
|
/** Mark this result as an expected, client-visible tool failure. */
|
|
98
116
|
isError: true;
|
|
99
117
|
},
|
|
100
|
-
): McpErrorResult<Structured>;
|
|
118
|
+
): McpErrorResult<Structured, Meta>;
|
|
101
119
|
/**
|
|
102
120
|
* Wrap rich content with required structured application data.
|
|
103
121
|
* @param value - Client content and JSON-compatible data; a successful output-schema result needs both.
|
|
104
122
|
* @returns A branded structured result; the server validates and converts it for the selected revision.
|
|
105
123
|
*/
|
|
106
|
-
export function mcpResult<
|
|
107
|
-
|
|
124
|
+
export function mcpResult<
|
|
125
|
+
Structured,
|
|
126
|
+
const Meta extends MetaObject = MetaObject,
|
|
127
|
+
>(
|
|
128
|
+
value: McpResultOptions<Structured, Meta> & {
|
|
108
129
|
/** Required application data, parsed by the declared output schema when present. */
|
|
109
130
|
structuredContent: Structured;
|
|
110
131
|
},
|
|
111
|
-
): McpStructuredResult<Structured>;
|
|
132
|
+
): McpStructuredResult<Structured, Meta>;
|
|
112
133
|
/**
|
|
113
134
|
* Wrap explicit client content without structured application data.
|
|
114
135
|
* @param value - Rich content; successful results of this form require a tool without an output schema.
|
|
115
136
|
* @returns A branded MCP result. Ordinary result-shaped objects are not treated as rich results.
|
|
116
137
|
*/
|
|
117
|
-
export function mcpResult
|
|
138
|
+
export function mcpResult<
|
|
139
|
+
Structured = never,
|
|
140
|
+
const Meta extends MetaObject = MetaObject,
|
|
141
|
+
>(value: McpResultOptions<never, Meta>): McpResult<Structured, Meta>;
|
|
118
142
|
/**
|
|
119
143
|
* Explicitly return MCP content instead of ordinary application JSON.
|
|
120
144
|
* The server validates the envelope, parses supplied output data, and converts it for the protocol revision.
|
|
@@ -122,11 +146,13 @@ export function mcpResult(value: McpResultOptions<never>): McpResult<never>;
|
|
|
122
146
|
* @param value - Content blocks, optional structured data, an error marker, and protocol metadata.
|
|
123
147
|
* @returns A frozen result wrapper; content is checked during server invocation.
|
|
124
148
|
*/
|
|
125
|
-
export function mcpResult<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
export function mcpResult<
|
|
150
|
+
Structured = never,
|
|
151
|
+
const Meta extends MetaObject = MetaObject,
|
|
152
|
+
>(value: McpResultOptions<Structured, Meta>): McpResult<Structured, Meta>;
|
|
153
|
+
export function mcpResult<
|
|
154
|
+
Structured = never,
|
|
155
|
+
const Meta extends MetaObject = MetaObject,
|
|
156
|
+
>(value: McpResultOptions<Structured, Meta>): McpResult<Structured, Meta> {
|
|
131
157
|
return new McpResult(value);
|
|
132
158
|
}
|