@toolgrain/mcp 0.3.0-alpha.1 → 0.3.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +10 -0
  3. package/dist/apps.d.ts +9 -0
  4. package/dist/apps.d.ts.map +1 -0
  5. package/dist/apps.js +21 -0
  6. package/dist/apps.js.map +1 -0
  7. package/dist/dispatcher.d.ts +2 -1
  8. package/dist/dispatcher.d.ts.map +1 -1
  9. package/dist/dispatcher.js +3 -3
  10. package/dist/dispatcher.js.map +1 -1
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/legacy/dispatcher.d.ts +2 -1
  16. package/dist/legacy/dispatcher.d.ts.map +1 -1
  17. package/dist/legacy/dispatcher.js +23 -8
  18. package/dist/legacy/dispatcher.js.map +1 -1
  19. package/dist/legacy/handlers.d.ts +1 -1
  20. package/dist/legacy/handlers.d.ts.map +1 -1
  21. package/dist/legacy/handlers.js +2 -2
  22. package/dist/legacy/handlers.js.map +1 -1
  23. package/dist/modern/dispatcher.d.ts +2 -1
  24. package/dist/modern/dispatcher.d.ts.map +1 -1
  25. package/dist/modern/dispatcher.js +22 -8
  26. package/dist/modern/dispatcher.js.map +1 -1
  27. package/dist/modern/handlers.d.ts +1 -1
  28. package/dist/modern/handlers.d.ts.map +1 -1
  29. package/dist/modern/handlers.js +2 -2
  30. package/dist/modern/handlers.js.map +1 -1
  31. package/dist/modern/results.d.ts +6 -0
  32. package/dist/modern/results.d.ts.map +1 -1
  33. package/dist/modern/results.js +9 -0
  34. package/dist/modern/results.js.map +1 -1
  35. package/dist/resources/definition.d.ts +45 -0
  36. package/dist/resources/definition.d.ts.map +1 -0
  37. package/dist/resources/definition.js +46 -0
  38. package/dist/resources/definition.js.map +1 -0
  39. package/dist/resources/registry.d.ts +13 -0
  40. package/dist/resources/registry.d.ts.map +1 -0
  41. package/dist/resources/registry.js +48 -0
  42. package/dist/resources/registry.js.map +1 -0
  43. package/dist/resources/requests.d.ts +13 -0
  44. package/dist/resources/requests.d.ts.map +1 -0
  45. package/dist/resources/requests.js +43 -0
  46. package/dist/resources/requests.js.map +1 -0
  47. package/dist/resources/validation.d.ts +33 -0
  48. package/dist/resources/validation.d.ts.map +1 -0
  49. package/dist/resources/validation.js +61 -0
  50. package/dist/resources/validation.js.map +1 -0
  51. package/dist/server.d.ts +7 -4
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +6 -3
  54. package/dist/server.js.map +1 -1
  55. package/dist/tools/context.d.ts +2 -2
  56. package/dist/tools/context.d.ts.map +1 -1
  57. package/dist/tools/context.js +1 -1
  58. package/dist/tools/rich-result.d.ts +19 -19
  59. package/dist/tools/rich-result.d.ts.map +1 -1
  60. package/dist/tools/rich-result.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/apps.ts +23 -0
  63. package/src/dispatcher.ts +4 -0
  64. package/src/index.ts +9 -0
  65. package/src/legacy/dispatcher.ts +55 -13
  66. package/src/legacy/handlers.ts +2 -1
  67. package/src/modern/dispatcher.ts +57 -21
  68. package/src/modern/handlers.ts +2 -1
  69. package/src/modern/results.ts +10 -0
  70. package/src/resources/definition.ts +92 -0
  71. package/src/resources/registry.ts +72 -0
  72. package/src/resources/requests.ts +59 -0
  73. package/src/resources/validation.ts +82 -0
  74. package/src/server.ts +11 -4
  75. package/src/tools/context.ts +2 -2
  76. package/src/tools/rich-result.ts +53 -27
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { AppSupport } from "./apps.js";
2
+ export { getAppSupport } from "./apps.js";
1
3
  export { validateTokenAudience } from "./auth/audience.js";
2
4
  export { createAuthAccessor } from "./auth/context.js";
3
5
  export type { AuthErrorCode } from "./auth/errors.js";
@@ -14,6 +16,13 @@ export { authUrl as validateAuthUrl } from "./auth/url.js";
14
16
  export type { ContextProvider } from "./context/definition.js";
15
17
  export { createContext } from "./context/definition.js";
16
18
  export type { ToolErrorDetails } from "./diagnostics.js";
19
+ export type {
20
+ Resource,
21
+ ResourceDefinition,
22
+ ResourceMetadata,
23
+ ResourceReadResult,
24
+ } from "./resources/definition.js";
25
+ export { resource } from "./resources/definition.js";
17
26
  export type { HandleOptions, Server, ServerOptions } from "./server.js";
18
27
  export { createServer } from "./server.js";
19
28
  export type {
@@ -10,7 +10,15 @@ import type { RequestScope } from "../dispatcher.js";
10
10
  import type { Request } from "../protocol/messages.js";
11
11
  import { ProtocolError } from "../protocol/responses.js";
12
12
  import { hasProtocolVersionMetadata } from "../protocol/version-metadata.js";
13
+ import type { ResourceRegistry } from "../resources/registry.js";
14
+ import {
15
+ listResources,
16
+ listResourceTemplates,
17
+ readResource,
18
+ requireResources,
19
+ } from "../resources/requests.js";
13
20
  import type { ToolAdapter } from "../tools/adapter.js";
21
+ import type { ToolContext } from "../tools/context.js";
14
22
  import { createToolContext } from "../tools/context.js";
15
23
  import { callTool, initialize, listTools, ping } from "./handlers.js";
16
24
  import { successResponse } from "./responses.js";
@@ -33,15 +41,36 @@ export function createLegacyDispatcher(
33
41
  identity: Implementation,
34
42
  instructions?: string,
35
43
  providers?: ContextRegistry,
44
+ resources?: ResourceRegistry,
36
45
  ) {
37
46
  return async (
38
47
  request: Request,
39
48
  scope: RequestScope,
40
49
  ): Promise<JSONRPCResponse | undefined> => {
50
+ const context = (params: {
51
+ _meta?: Record<string, unknown> | undefined;
52
+ }): ToolContext =>
53
+ createToolContext(
54
+ scope.signal,
55
+ {
56
+ id: request.id,
57
+ protocolVersion: LEGACY_PROTOCOL_VERSION,
58
+ ...(scope.client === undefined ? {} : { client: scope.client }),
59
+ meta: params._meta ?? {},
60
+ },
61
+ scope.auth,
62
+ providers,
63
+ scope.reportProgress,
64
+ );
41
65
  let result: Result | undefined;
42
66
  switch (request.method) {
43
67
  case "initialize":
44
- result = initialize(request.params, identity, instructions);
68
+ result = initialize(
69
+ request.params,
70
+ identity,
71
+ instructions,
72
+ resources !== undefined,
73
+ );
45
74
  break;
46
75
  case "ping":
47
76
  result = ping(request.params);
@@ -50,19 +79,32 @@ export function createLegacyDispatcher(
50
79
  result = listTools(request.params, tools);
51
80
  break;
52
81
  case "tools/call":
53
- result = await callTool(request.params, tools, (params) =>
54
- createToolContext(
55
- scope.signal,
56
- {
57
- id: request.id,
58
- protocolVersion: LEGACY_PROTOCOL_VERSION,
59
- ...(scope.client === undefined ? {} : { client: scope.client }),
60
- meta: params._meta ?? {},
61
- },
62
- scope.auth,
63
- providers,
64
- scope.reportProgress,
82
+ result = await callTool(request.params, tools, context);
83
+ break;
84
+ case "resources/list": {
85
+ const listed = listResources(
86
+ request.params,
87
+ requireResources(resources),
88
+ );
89
+ result = {
90
+ resources: listed.resources.map(
91
+ ({ icons: _icons, ...entry }) => entry,
65
92
  ),
93
+ };
94
+ break;
95
+ }
96
+ case "resources/templates/list":
97
+ result = listResourceTemplates(
98
+ request.params,
99
+ requireResources(resources),
100
+ );
101
+ break;
102
+ case "resources/read":
103
+ result = await readResource(
104
+ request.params,
105
+ requireResources(resources),
106
+ context,
107
+ -32002,
66
108
  );
67
109
  break;
68
110
  default:
@@ -24,6 +24,7 @@ export function initialize(
24
24
  params: unknown,
25
25
  identity: Implementation,
26
26
  instructions?: string,
27
+ resources = false,
27
28
  ): InitializeResult {
28
29
  if (!initializeParams.safeParse(params).success)
29
30
  throw new ProtocolError(
@@ -32,7 +33,7 @@ export function initialize(
32
33
  );
33
34
  return {
34
35
  protocolVersion: LEGACY_PROTOCOL_VERSION,
35
- capabilities: { tools: {} },
36
+ capabilities: { tools: {}, ...(resources ? { resources: {} } : {}) },
36
37
  serverInfo: { ...identity },
37
38
  ...(instructions === undefined ? {} : { instructions }),
38
39
  };
@@ -13,17 +13,27 @@ import type { ContextRegistry } from "../context/registry.js";
13
13
  import type { RequestScope } from "../dispatcher.js";
14
14
  import type { Request } from "../protocol/messages.js";
15
15
  import { ProtocolError } from "../protocol/responses.js";
16
+ import type { ResourceRegistry } from "../resources/registry.js";
17
+ import {
18
+ listResources,
19
+ listResourceTemplates,
20
+ readResource,
21
+ requireResources,
22
+ } from "../resources/requests.js";
16
23
  import type { ToolAdapter } from "../tools/adapter.js";
24
+ import type { ToolContext } from "../tools/context.js";
17
25
  import { createToolContext, toolClient } from "../tools/context.js";
18
26
  import { callTool, discover, listTools } from "./handlers.js";
19
27
  import { metadata } from "./metadata.js";
20
28
  import { successResponse } from "./responses.js";
29
+ import { resourceResult } from "./results.js";
21
30
 
22
31
  export function createModernDispatcher(
23
32
  tools: ToolAdapter,
24
33
  identity: Implementation,
25
34
  instructions?: string,
26
35
  providers?: ContextRegistry,
36
+ resources?: ResourceRegistry,
27
37
  ) {
28
38
  return async (
29
39
  request: Request,
@@ -39,37 +49,63 @@ export function createModernDispatcher(
39
49
  "Unsupported protocol version",
40
50
  { supported: [...SUPPORTED_PROTOCOL_VERSIONS], requested: revision },
41
51
  );
52
+ const context = (params: {
53
+ _meta?: Record<string, unknown> | undefined;
54
+ }): ToolContext =>
55
+ createToolContext(
56
+ scope.signal,
57
+ {
58
+ id: request.id,
59
+ protocolVersion: PROTOCOL_VERSION,
60
+ client: toolClient(
61
+ parsed.data["io.modelcontextprotocol/clientInfo"] as
62
+ | Implementation
63
+ | undefined,
64
+ parsed.data[
65
+ "io.modelcontextprotocol/clientCapabilities"
66
+ ] as ClientCapabilities,
67
+ ),
68
+ meta: params._meta ?? {},
69
+ },
70
+ scope.auth,
71
+ providers,
72
+ scope.reportProgress,
73
+ );
42
74
  let result: Result | undefined;
43
75
  switch (request.method) {
44
76
  case "server/discover":
45
- result = discover(request.params, instructions);
77
+ result = discover(
78
+ request.params,
79
+ instructions,
80
+ resources !== undefined,
81
+ );
46
82
  break;
47
83
  case "tools/list":
48
84
  result = listTools(request.params, tools);
49
85
  break;
50
86
  case "tools/call":
51
- result = await callTool(request.params, tools, (params) =>
52
- createToolContext(
53
- scope.signal,
54
- {
55
- id: request.id,
56
- protocolVersion: PROTOCOL_VERSION,
57
- client: toolClient(
58
- parsed.data["io.modelcontextprotocol/clientInfo"] as
59
- | Implementation
60
- | undefined,
61
- parsed.data[
62
- "io.modelcontextprotocol/clientCapabilities"
63
- ] as ClientCapabilities,
64
- ),
65
- meta: params._meta ?? {},
66
- },
67
- scope.auth,
68
- providers,
69
- scope.reportProgress,
70
- ),
87
+ result = await callTool(request.params, tools, context);
88
+ break;
89
+ case "resources/list":
90
+ result = resourceResult(
91
+ listResources(request.params, requireResources(resources)),
92
+ );
93
+ break;
94
+ case "resources/templates/list":
95
+ result = resourceResult(
96
+ listResourceTemplates(request.params, requireResources(resources)),
97
+ );
98
+ break;
99
+ case "resources/read": {
100
+ const value = await readResource(
101
+ request.params,
102
+ requireResources(resources),
103
+ context,
104
+ INVALID_PARAMS,
71
105
  );
106
+ result = value === undefined ? undefined : resourceResult(value);
72
107
  break;
108
+ }
73
109
  default:
74
110
  throw new ProtocolError(METHOD_NOT_FOUND, "Method not found");
75
111
  }
@@ -14,6 +14,7 @@ import { toolResult } from "./results.js";
14
14
  export function discover(
15
15
  params: unknown,
16
16
  instructions?: string,
17
+ resources = false,
17
18
  ): DiscoverResult {
18
19
  if (!commonParams.safeParse(params).success)
19
20
  throw new ProtocolError(INVALID_PARAMS, "Invalid discovery parameters");
@@ -22,7 +23,7 @@ export function discover(
22
23
  ttlMs: 0,
23
24
  cacheScope: "private",
24
25
  supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
25
- capabilities: { tools: {} },
26
+ capabilities: { tools: {}, ...(resources ? { resources: {} } : {}) },
26
27
  ...(instructions === undefined ? {} : { instructions }),
27
28
  };
28
29
  }
@@ -4,3 +4,13 @@ import type { ToolExecutionResult } from "../tools/results.js";
4
4
  export function toolResult(result: ToolExecutionResult): CallToolResult {
5
5
  return { resultType: "complete", ...result };
6
6
  }
7
+
8
+ /** Resource results do not enable caching until applications have a cache-policy API. */
9
+ export function resourceResult<Result extends object>(result: Result) {
10
+ return {
11
+ ...result,
12
+ resultType: "complete" as const,
13
+ ttlMs: 0,
14
+ cacheScope: "private" as const,
15
+ };
16
+ }
@@ -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, metadata, and exposable schemas immediately.
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>();
@@ -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;
@@ -70,7 +70,7 @@ export interface ToolContextOptions {
70
70
  }
71
71
 
72
72
  /**
73
- * Build an unauthenticated context for direct `execute()` calls, for example in tests.
73
+ * Build an unauthenticated context for direct tool `execute()` or resource `read()` calls.
74
74
  * Request metadata is copied and deeply frozen; each call creates a fresh provider cache.
75
75
  * Direct execution still bypasses server authorization, validation, and MCP conversion.
76
76
  *