typespec-http-mcp 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 +141 -0
- package/dist/src/annotations.d.ts +33 -0
- package/dist/src/annotations.js +41 -0
- package/dist/src/bodies.d.ts +69 -0
- package/dist/src/bodies.js +94 -0
- package/dist/src/coexistence.d.ts +13 -0
- package/dist/src/coexistence.js +91 -0
- package/dist/src/emitter.d.ts +15 -0
- package/dist/src/emitter.js +237 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/lib.d.ts +119 -0
- package/dist/src/lib.js +278 -0
- package/dist/src/names.d.ts +34 -0
- package/dist/src/names.js +55 -0
- package/dist/src/refusals.d.ts +52 -0
- package/dist/src/refusals.js +167 -0
- package/dist/src/runtime.d.ts +191 -0
- package/dist/src/runtime.js +280 -0
- package/dist/src/select.d.ts +49 -0
- package/dist/src/select.js +59 -0
- package/dist/src/statuses.d.ts +35 -0
- package/dist/src/statuses.js +29 -0
- package/dist/src/tools.d.ts +37 -0
- package/dist/src/tools.js +545 -0
- package/dist/src/tsp-index.d.ts +14 -0
- package/dist/src/tsp-index.js +14 -0
- package/lib/main.tsp +3 -0
- package/package.json +95 -0
- package/src/runtime.ts +475 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an operation is called on the MCP surface.
|
|
3
|
+
*
|
|
4
|
+
* **The source is the operation id the DOCUMENT publishes, and it is never re-spelled.** An
|
|
5
|
+
* operation id is a contract fact; a generated second spelling of it is a name no reader of the
|
|
6
|
+
* document can predict. So a name this emitter cannot carry is REFUSED rather than rewritten -- the
|
|
7
|
+
* same rule the sibling emitter holds for path templates, and for the same reason: a surface that
|
|
8
|
+
* works and is wrong is strictly worse than one that is absent and says so.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toolNameFor(operationId: string, prefix: string | undefined): string;
|
|
11
|
+
export declare function isRepresentable(name: string): boolean;
|
|
12
|
+
export interface NamedTools {
|
|
13
|
+
/** Operation id to tool name, for everything that survived. */
|
|
14
|
+
readonly names: ReadonlyMap<string, string>;
|
|
15
|
+
/** Operation ids refused because the name is unrepresentable. */
|
|
16
|
+
readonly unrepresentable: readonly {
|
|
17
|
+
readonly operationId: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Tool names two operations both produced. BOTH operations are refused. */
|
|
21
|
+
readonly collisions: readonly {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly first: string;
|
|
24
|
+
readonly second: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Name every selected operation, and report what cannot be named.
|
|
29
|
+
*
|
|
30
|
+
* **A collision refuses BOTH operations, deliberately.** Keeping one and dropping the other would
|
|
31
|
+
* make the tool surface depend on which operation the emitter happened to reach first -- a surface
|
|
32
|
+
* that answers, and answers as the wrong operation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nameTools(selected: readonly string[], prefix: string | undefined): NamedTools;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an operation is called on the MCP surface.
|
|
3
|
+
*
|
|
4
|
+
* **The source is the operation id the DOCUMENT publishes, and it is never re-spelled.** An
|
|
5
|
+
* operation id is a contract fact; a generated second spelling of it is a name no reader of the
|
|
6
|
+
* document can predict. So a name this emitter cannot carry is REFUSED rather than rewritten -- the
|
|
7
|
+
* same rule the sibling emitter holds for path templates, and for the same reason: a surface that
|
|
8
|
+
* works and is wrong is strictly worse than one that is absent and says so.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* MCP's own rule: 1 to 128 characters of `A-Z`, `a-z`, `0-9`, `_`, `-` or `.`.
|
|
12
|
+
*
|
|
13
|
+
* TypeSpec permits characters this does not -- `$` reaches an operation id through a template
|
|
14
|
+
* instantiation -- so this refuses for real specs rather than only in theory.
|
|
15
|
+
*/
|
|
16
|
+
const MCP_TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
17
|
+
export function toolNameFor(operationId, prefix) {
|
|
18
|
+
// Prepended verbatim: the author owns the separator, because any separator this emitter chose
|
|
19
|
+
// would be a spelling decision the document does not state.
|
|
20
|
+
return prefix === undefined ? operationId : `${prefix}${operationId}`;
|
|
21
|
+
}
|
|
22
|
+
export function isRepresentable(name) {
|
|
23
|
+
return MCP_TOOL_NAME.test(name);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Name every selected operation, and report what cannot be named.
|
|
27
|
+
*
|
|
28
|
+
* **A collision refuses BOTH operations, deliberately.** Keeping one and dropping the other would
|
|
29
|
+
* make the tool surface depend on which operation the emitter happened to reach first -- a surface
|
|
30
|
+
* that answers, and answers as the wrong operation.
|
|
31
|
+
*/
|
|
32
|
+
export function nameTools(selected, prefix) {
|
|
33
|
+
const names = new Map();
|
|
34
|
+
const unrepresentable = [];
|
|
35
|
+
const byName = new Map();
|
|
36
|
+
const collisions = [];
|
|
37
|
+
for (const operationId of selected) {
|
|
38
|
+
const name = toolNameFor(operationId, prefix);
|
|
39
|
+
if (!isRepresentable(name)) {
|
|
40
|
+
unrepresentable.push({ operationId, name });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const first = byName.get(name);
|
|
44
|
+
if (first !== undefined) {
|
|
45
|
+
collisions.push({ name, first, second: operationId });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
byName.set(name, operationId);
|
|
49
|
+
names.set(operationId, name);
|
|
50
|
+
}
|
|
51
|
+
// Both sides of a collision are refused, so the winner recorded above is removed too.
|
|
52
|
+
for (const collision of collisions)
|
|
53
|
+
names.delete(collision.first);
|
|
54
|
+
return { names, unrepresentable, collisions };
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Program } from "@typespec/compiler";
|
|
2
|
+
import type { HttpOperation } from "@typespec/http";
|
|
3
|
+
import type { EmittedRoute, RouteSchemaNames } from "typespec-http-zod";
|
|
4
|
+
export type Refusal = {
|
|
5
|
+
readonly kind: "binary-tool-input";
|
|
6
|
+
readonly types: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "non-json-request";
|
|
9
|
+
readonly types: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "non-object-body";
|
|
12
|
+
readonly types: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "streamed-tool-output";
|
|
15
|
+
readonly types: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "binary-tool-output";
|
|
18
|
+
readonly types: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Why this operation cannot be a tool, or `undefined` where it can.
|
|
22
|
+
*
|
|
23
|
+
* **Not a refusal: a reserved-expansion path parameter.** That is a ROUTING fact -- it decides how a
|
|
24
|
+
* router matches a URL. MCP has no URI template, and the handler receives the same string either
|
|
25
|
+
* way. The sibling server emitter refuses it and should; that the two emitters have different
|
|
26
|
+
* refusal sets from one intermediate representation is the split working, not a gap.
|
|
27
|
+
*/
|
|
28
|
+
export declare function refusalFor(program: Program, route: EmittedRoute, operation: HttpOperation): Refusal | undefined;
|
|
29
|
+
/** A quality warning, with what its message names. */
|
|
30
|
+
export type Warning = {
|
|
31
|
+
readonly code: "headers-only-success" | "tool-without-description";
|
|
32
|
+
readonly format: object;
|
|
33
|
+
} | {
|
|
34
|
+
readonly code: "unvalidated-response-media-type";
|
|
35
|
+
readonly format: {
|
|
36
|
+
readonly status: string;
|
|
37
|
+
readonly types: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Quality warnings: the tool is still emitted.
|
|
42
|
+
*
|
|
43
|
+
* `headers-only-success` fires where a 2xx or 3xx response carries headers and no body, so everything
|
|
44
|
+
* that response produces lands in `_meta` -- which a model does not read. Read off each declared
|
|
45
|
+
* response, ranges included, because a `2XX` with a header and no body answers the same way.
|
|
46
|
+
*
|
|
47
|
+
* `unvalidated-response-media-type` fires once per status whose body under some media type is a model
|
|
48
|
+
* no serialisation derives from -- an `Item` as `application/xml`. The handler supplies that text and
|
|
49
|
+
* the tool answers it unvalidated. The sibling server emitter raises the same warning, under the same
|
|
50
|
+
* code, for the same responses.
|
|
51
|
+
*/
|
|
52
|
+
export declare function warningsFor(route: EmittedRoute, names: RouteSchemaNames, described: boolean): readonly Warning[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { getDiscriminator } from "@typespec/compiler";
|
|
2
|
+
import { hasContentBlock, unvalidatedResponsesOf } from "./bodies.js";
|
|
3
|
+
/**
|
|
4
|
+
* What this emitter cannot express as a tool, and what it can express but should warn about.
|
|
5
|
+
*
|
|
6
|
+
* **The two are kept apart deliberately.** A REFUSAL removes the operation from the tool surface, so
|
|
7
|
+
* it must be counted: `emitted + refused === selected` is the invariant that makes a silent drop
|
|
8
|
+
* arithmetically impossible. A WARNING leaves the tool in place and says something about its quality.
|
|
9
|
+
* Conflating them would let a quality complaint quietly delete a tool.
|
|
10
|
+
*/
|
|
11
|
+
/** A media type a tool can actually send. `application/json`, and the `+json` structured suffix. */
|
|
12
|
+
function isJson(contentType) {
|
|
13
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
14
|
+
return type === "application/json" || type.endsWith("+json");
|
|
15
|
+
}
|
|
16
|
+
function isMultipart(contentType) {
|
|
17
|
+
return (contentType.split(";")[0]?.trim().toLowerCase() ?? "").startsWith("multipart/");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a request body can be MERGED into a tool's argument object.
|
|
21
|
+
*
|
|
22
|
+
* **A tool's arguments are one object, and the document states the body separately from the
|
|
23
|
+
* parameters, so the two are merged here.** A body that is a scalar, an enum, a union or an array
|
|
24
|
+
* has no properties to merge -- `@body body: string` produces a `ZodString`, and spreading its
|
|
25
|
+
* (absent) shape emits a file that does not compile. Naming it under an invented property would
|
|
26
|
+
* publish a contract no reader of the document could predict, and would break the shared handler
|
|
27
|
+
* signature, which takes the bare value.
|
|
28
|
+
*
|
|
29
|
+
* A body the library already NAMES is safe whatever its type: it is nested rather than merged.
|
|
30
|
+
*/
|
|
31
|
+
function isMergeableBody(program, type) {
|
|
32
|
+
if (type === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
if (type.kind !== "Model")
|
|
35
|
+
return false;
|
|
36
|
+
// An array is a Model with an integer-keyed indexer, and has no properties to merge.
|
|
37
|
+
if (type.indexer?.key?.name === "integer")
|
|
38
|
+
return false;
|
|
39
|
+
/**
|
|
40
|
+
* A model carrying `@discriminator` is emitted as a UNION of its variants, not as an object --
|
|
41
|
+
* measured: `z.discriminatedUnion("kind", [...])`, which has no shape. Which variant arrived is
|
|
42
|
+
* decided by a value, so there is no single set of properties to merge into the arguments.
|
|
43
|
+
*/
|
|
44
|
+
if (getDiscriminator(program, type) !== undefined)
|
|
45
|
+
return false;
|
|
46
|
+
/**
|
|
47
|
+
* **A TYPED index signature does not survive the merge.** A model carrying declared keys beside a
|
|
48
|
+
* `Record<float32>` spreads its shape into the argument object, and the index type is left behind:
|
|
49
|
+
* the merge publishes `{ [x: string]: unknown }` where the handler declares
|
|
50
|
+
* `{ [x: string]: number }`. Measured on the corpus, that is a `TS2345` in a file the consumer
|
|
51
|
+
* cannot edit.
|
|
52
|
+
*
|
|
53
|
+
* An `unknown`-valued indexer is ordinary model openness and merges without loss, so it is not
|
|
54
|
+
* refused -- the same distinction the library draws between openness and a dictionary.
|
|
55
|
+
*/
|
|
56
|
+
for (let model = type; model !== undefined; model = model.baseModel) {
|
|
57
|
+
const indexer = model.indexer;
|
|
58
|
+
if (indexer?.key?.name === "string" && indexer.value?.kind !== "Intrinsic")
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Why this operation cannot be a tool, or `undefined` where it can.
|
|
65
|
+
*
|
|
66
|
+
* **Not a refusal: a reserved-expansion path parameter.** That is a ROUTING fact -- it decides how a
|
|
67
|
+
* router matches a URL. MCP has no URI template, and the handler receives the same string either
|
|
68
|
+
* way. The sibling server emitter refuses it and should; that the two emitters have different
|
|
69
|
+
* refusal sets from one intermediate representation is the split working, not a gap.
|
|
70
|
+
*/
|
|
71
|
+
export function refusalFor(program, route, operation) {
|
|
72
|
+
const types = route.requestContentTypes;
|
|
73
|
+
/**
|
|
74
|
+
* A raw body is bytes or a stream; a multipart body is parts, one of which is usually a file.
|
|
75
|
+
* Neither is a JSON value, and encoding one into a string argument would put an upload through a
|
|
76
|
+
* model's context window under a contract the document never stated.
|
|
77
|
+
*/
|
|
78
|
+
if (route.rawBodyProperty !== undefined) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "binary-tool-input",
|
|
81
|
+
types: types.length === 0 ? "a raw body" : types.join(", "),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (types.some(isMultipart)) {
|
|
85
|
+
return { kind: "binary-tool-input", types: types.join(", ") };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Only where NO JSON type is declared beside them. Where one is, the tool sends JSON and the
|
|
89
|
+
* others are simply unreachable from MCP, which is not a defect.
|
|
90
|
+
*/
|
|
91
|
+
if (types.length > 0 && !types.some(isJson)) {
|
|
92
|
+
return { kind: "non-json-request", types: types.join(", ") };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A body the library named is nested under that name and is safe whatever its type. An unnamed
|
|
96
|
+
* body is merged into the argument object, so it must have properties to merge.
|
|
97
|
+
*/
|
|
98
|
+
if (route.bodyProperty === undefined) {
|
|
99
|
+
const body = operation.parameters.body?.type;
|
|
100
|
+
if (!isMergeableBody(program, body)) {
|
|
101
|
+
return { kind: "non-object-body", types: body?.kind ?? "unknown" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* **A stream is not one message, and a tool result is.** An SSE or JSONL response is unbounded and
|
|
106
|
+
* arrives over time; a tool call answers once. Buffering it would wait on a stream that may never
|
|
107
|
+
* end, and answering its first event would publish a contract no reader of the document could
|
|
108
|
+
* predict.
|
|
109
|
+
*/
|
|
110
|
+
const streamed = route.responses.filter((response) => response.streamed);
|
|
111
|
+
if (streamed.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "streamed-tool-output",
|
|
114
|
+
types: streamed.flatMap((response) => response.contentTypes).join(", ") || "a stream",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* **Raw bytes are a tool result only where MCP has a block for them.** An `image/*` or `audio/*`
|
|
119
|
+
* body is answered as an `image` or `audio` content block, which carries the data and its media
|
|
120
|
+
* type. Any other binary body -- `application/octet-stream`, a PDF -- could only be an embedded
|
|
121
|
+
* resource, and a resource needs a URI the document never states. Base64-ing the bytes into a text
|
|
122
|
+
* block would put a download through a model's context window under an encoding nothing published.
|
|
123
|
+
* Refused if ANY declared response offers such a type, because a handler may answer with any of
|
|
124
|
+
* them.
|
|
125
|
+
*/
|
|
126
|
+
const unanswerable = route.responses
|
|
127
|
+
.filter((response) => response.binary)
|
|
128
|
+
.flatMap((response) => response.contentTypes)
|
|
129
|
+
.filter((type) => !hasContentBlock(type));
|
|
130
|
+
if (unanswerable.length > 0) {
|
|
131
|
+
return { kind: "binary-tool-output", types: [...new Set(unanswerable)].join(", ") };
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Quality warnings: the tool is still emitted.
|
|
137
|
+
*
|
|
138
|
+
* `headers-only-success` fires where a 2xx or 3xx response carries headers and no body, so everything
|
|
139
|
+
* that response produces lands in `_meta` -- which a model does not read. Read off each declared
|
|
140
|
+
* response, ranges included, because a `2XX` with a header and no body answers the same way.
|
|
141
|
+
*
|
|
142
|
+
* `unvalidated-response-media-type` fires once per status whose body under some media type is a model
|
|
143
|
+
* no serialisation derives from -- an `Item` as `application/xml`. The handler supplies that text and
|
|
144
|
+
* the tool answers it unvalidated. The sibling server emitter raises the same warning, under the same
|
|
145
|
+
* code, for the same responses.
|
|
146
|
+
*/
|
|
147
|
+
export function warningsFor(route, names, described) {
|
|
148
|
+
const warnings = [];
|
|
149
|
+
const headersOnlySuccess = route.responses.some((response) => isSuccess(response.status) && response.schema === undefined && response.headers.length > 0);
|
|
150
|
+
if (headersOnlySuccess)
|
|
151
|
+
warnings.push({ code: "headers-only-success", format: {} });
|
|
152
|
+
for (const { status, types } of unvalidatedResponsesOf(route, names)) {
|
|
153
|
+
warnings.push({
|
|
154
|
+
code: "unvalidated-response-media-type",
|
|
155
|
+
format: { status: String(status), types: types.join(", ") },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!described)
|
|
159
|
+
warnings.push({ code: "tool-without-description", format: {} });
|
|
160
|
+
return warnings;
|
|
161
|
+
}
|
|
162
|
+
/** A 2xx or 3xx status key, exact or ranged. */
|
|
163
|
+
function isSuccess(status) {
|
|
164
|
+
if (typeof status === "number")
|
|
165
|
+
return status >= 200 && status < 400;
|
|
166
|
+
return status === "2XX" || status === "3XX";
|
|
167
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { CallToolResult, McpServer, ServerContext } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { ZodError, ZodType } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* **What generated code imports at run time -- and the only thing this package puts in its output.**
|
|
5
|
+
*
|
|
6
|
+
* **Kept deliberately tiny, and deliberately free of every TypeSpec import.** The main entry point
|
|
7
|
+
* is an emitter: it pulls in `@typespec/compiler` and exists at build time. This module is what an
|
|
8
|
+
* *application* touches, and making it reach the compiler would put a build-time dependency into a
|
|
9
|
+
* server's runtime graph for the sake of a few declarations. `test/packaging.test.ts` asserts the
|
|
10
|
+
* separation rather than trusting it.
|
|
11
|
+
*
|
|
12
|
+
* **Always emitted, never substituted.** `runtime-module` used to point the generated file at a
|
|
13
|
+
* module of the application's own, which made the application own a copy of the arm selection and
|
|
14
|
+
* the result rules below. Nothing it was for needs it now: the caller context is inferred from
|
|
15
|
+
* `deps.context`, and what a handler returns is the union of the responses the document declares.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The SDK types the generated signatures name, re-exported so emitted code has ONE specifier.
|
|
19
|
+
*
|
|
20
|
+
* **`export type`, not `export`.** Measured: `McpServer`, `ServerContext` and `CallToolResult` are
|
|
21
|
+
* type-only exports of `@modelcontextprotocol/server`; only `isCallToolResult` is a runtime value
|
|
22
|
+
* among them. A value re-export of any of these compiles and then fails at run time with
|
|
23
|
+
* `SyntaxError: The requested module does not provide an export named ...`.
|
|
24
|
+
*/
|
|
25
|
+
export type { CallToolResult, McpServer, ServerContext };
|
|
26
|
+
/**
|
|
27
|
+
* One status an operation may answer with, and what the document publishes for it.
|
|
28
|
+
*
|
|
29
|
+
* **Declared here rather than imported from `typespec-http-zod`, deliberately.** Emitted output must
|
|
30
|
+
* never import this emitter or the library it runs -- a consumer installs both as devDependencies,
|
|
31
|
+
* so a runtime import of either is a broken install. `test/packaging.test.ts` asserts that. The
|
|
32
|
+
* emitted `schemas.gen.ts` declares the same shape locally, so it depends on no runtime at all.
|
|
33
|
+
*
|
|
34
|
+
* The cost is a second declaration of one shape, and `test/runtime/parity.test.ts` is what stops the
|
|
35
|
+
* two drifting: it grades this `armFor` against the library's own for every arm shape.
|
|
36
|
+
*
|
|
37
|
+
* **The arms arrive in OpenAPI's precedence order, so the FIRST match is the right one** -- an exact
|
|
38
|
+
* code, then a range, then the catch-all. `schema: undefined` is a statement rather than an absence:
|
|
39
|
+
* a response the document says carries no body has no validator.
|
|
40
|
+
*/
|
|
41
|
+
export interface ResponseArm {
|
|
42
|
+
/** Every media type this response offers, including a single one. Absent where there is no body. */
|
|
43
|
+
readonly contentTypes?: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* The headers this response declares, by the WIRE name the response sets. `optional` is the
|
|
46
|
+
* document's `required: false`. Absent where the response declares none.
|
|
47
|
+
*/
|
|
48
|
+
readonly headers?: readonly {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly optional: boolean;
|
|
51
|
+
}[];
|
|
52
|
+
readonly status: number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
|
|
53
|
+
readonly schema: ZodType | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The arm that answers for a status, in the document's own precedence order.
|
|
57
|
+
*
|
|
58
|
+
* Exact code, then the `NXX` range, then `default`. Sorting the rule out once here is what stops
|
|
59
|
+
* every application re-deriving it, and getting it silently wrong when a `404` and a `4XX` both apply.
|
|
60
|
+
*/
|
|
61
|
+
export declare function armFor(arms: readonly ResponseArm[], status: number): ResponseArm | undefined;
|
|
62
|
+
/** An OpenAPI security requirement: scheme name to the scopes it demands. */
|
|
63
|
+
export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
|
|
64
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
65
|
+
/** What the document declares a tool may answer with, handed to {@link toolResult}. */
|
|
66
|
+
export interface ToolResponses {
|
|
67
|
+
/** The operation's declared responses, in OpenAPI's precedence order. */
|
|
68
|
+
readonly arms: readonly ResponseArm[];
|
|
69
|
+
/**
|
|
70
|
+
* The schema this tool PUBLISHED, or `undefined` where it published none.
|
|
71
|
+
*
|
|
72
|
+
* Carried separately from the arms because MCP obliges a tool declaring an `outputSchema` to
|
|
73
|
+
* return conforming `structuredContent`, and which arm's schema was published is a decision the
|
|
74
|
+
* emitter made rather than something readable off the arms.
|
|
75
|
+
*/
|
|
76
|
+
readonly outputSchema: ZodType | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* How a body is answered under a media type that is not JSON, keyed by the arm's status key
|
|
79
|
+
* (`"200"`, `"4XX"`, `"default"`), and present only where one applies.
|
|
80
|
+
*
|
|
81
|
+
* **Carried because an arm cannot say it.** The arm names the media types and the schema, and
|
|
82
|
+
* neither tells a string body apart from raw bytes or from a model the handler serialised: an
|
|
83
|
+
* `image/png` body and an `application/xml` one both arrive as a schema and a non-JSON type. The
|
|
84
|
+
* emitter knows, from what the document declares the body to be.
|
|
85
|
+
*
|
|
86
|
+
* - `"text"`: a string, parsed against the arm's schema and answered as a `text` block;
|
|
87
|
+
* - `"binary"`: raw bytes, answered as an `image` or `audio` block.
|
|
88
|
+
*
|
|
89
|
+
* A status with no entry answers a non-JSON body as the text the handler supplied, unvalidated. A
|
|
90
|
+
* body served as JSON needs no entry, because the served media type says so.
|
|
91
|
+
*/
|
|
92
|
+
readonly bodies?: {
|
|
93
|
+
readonly [status: string]: "text" | "binary";
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A response body that does not match the schema the document publishes for its status.
|
|
98
|
+
*
|
|
99
|
+
* **Thrown, so the application decides what a contract failure answers with**, the same class and
|
|
100
|
+
* the same fields as the sibling server emitter's. The MCP server turns a thrown error into a
|
|
101
|
+
* tool-execution error; an application that wants something else catches it around `registerTools`'
|
|
102
|
+
* handlers.
|
|
103
|
+
*
|
|
104
|
+
* `issues` are Zod's, so they carry paths and codes. They also carry the offending VALUES; an
|
|
105
|
+
* application that logs them should decide whether its results may contain anything it would not
|
|
106
|
+
* log.
|
|
107
|
+
*/
|
|
108
|
+
export declare class ResponseContractError extends Error {
|
|
109
|
+
readonly operationId: string;
|
|
110
|
+
readonly status: number;
|
|
111
|
+
readonly issues: ZodError["issues"];
|
|
112
|
+
constructor(operationId: string, status: number, issues: ZodError["issues"]);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A handler answered with a status its operation does not declare.
|
|
116
|
+
*
|
|
117
|
+
* **Unreachable from a typed handler.** The generated `Tools` interface types every result as the
|
|
118
|
+
* union of the declared statuses, so a literal outside it does not compile. This is what a CAST
|
|
119
|
+
* produces, or a result that crossed a boundary the type system cannot see into. Thrown rather than
|
|
120
|
+
* answered, because answering would publish a status the contract does not state.
|
|
121
|
+
*/
|
|
122
|
+
export declare class UndeclaredStatusError extends Error {
|
|
123
|
+
readonly operationId: string;
|
|
124
|
+
readonly result: unknown;
|
|
125
|
+
constructor(operationId: string, result: unknown);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The MCP result for what a handler returned: `{ status, body, headers }` for a response the
|
|
129
|
+
* document declares.
|
|
130
|
+
*
|
|
131
|
+
* **In the runtime rather than in `deps`, on the same test the sibling emitter applies**: every rule
|
|
132
|
+
* below is published -- by the document (the arms, their bodies, their headers) or by the MCP
|
|
133
|
+
* specification (the result shape, its content blocks, `isError`, the text mirror). An application
|
|
134
|
+
* forced to supply this would be re-implementing two standards, and getting either wrong differently
|
|
135
|
+
* from everybody else.
|
|
136
|
+
*
|
|
137
|
+
* **What is answered is the PARSED body, not the one the handler returned**, as on the HTTP surface:
|
|
138
|
+
* a schema that strips undeclared keys strips them here too, so a field the document does not publish
|
|
139
|
+
* does not reach a model.
|
|
140
|
+
*
|
|
141
|
+
* Asynchronous because a binary body may be a stream, which is read to its end before it is answered.
|
|
142
|
+
*/
|
|
143
|
+
export declare function toolResult(declared: ToolResponses, operationId: string, result: unknown): Promise<CallToolResult>;
|
|
144
|
+
/**
|
|
145
|
+
* What the emitter cannot derive from the document, and the application must answer.
|
|
146
|
+
*
|
|
147
|
+
* The admission test for a member is the sibling emitter's: *the generated code cannot proceed
|
|
148
|
+
* without an answer*. Everything derivable lives above, in the runtime.
|
|
149
|
+
*
|
|
150
|
+
* **`C` is the caller context, and `registerTools` INFERS it from `context`.** An application whose
|
|
151
|
+
* `context` returns a `Caller` has handlers typed `(ctx: Caller, input)`, with nothing to declare.
|
|
152
|
+
*/
|
|
153
|
+
export interface ToolDeps<C = unknown> {
|
|
154
|
+
/**
|
|
155
|
+
* Whether the caller this server was built for may SEE this tool.
|
|
156
|
+
*
|
|
157
|
+
* **Consulted at REGISTRATION, not at call time.** The MCP specification says the tool set may
|
|
158
|
+
* vary by the authorisation presented on the request, since credentials are per-request input
|
|
159
|
+
* rather than connection state -- so the idiomatic gesture is that a caller does not see a tool
|
|
160
|
+
* they cannot call, rather than being refused after calling it. That is a genuine divergence
|
|
161
|
+
* from an HTTP surface, where a 403 is the right answer and middleware is the right shape.
|
|
162
|
+
*
|
|
163
|
+
* Receives the `security` the document publishes for the operation: satisfying ANY ONE requirement
|
|
164
|
+
* authorises, and every scheme WITHIN a requirement must be satisfied together. An anonymous
|
|
165
|
+
* alternative is the empty requirement `{}`, which every caller satisfies: `@useAuth(NoAuth)` is
|
|
166
|
+
* `[{}]` and `NoAuth | BearerAuth` is `[{}, { BearerAuth: [] }]`. An operation declaring no auth
|
|
167
|
+
* at all is `[]`. Both are facts rather than absences, because an application may still choose to
|
|
168
|
+
* hide a public tool from an anonymous caller.
|
|
169
|
+
*
|
|
170
|
+
* A grant that selects nothing is not an error: nothing is registered and `tools/list` answers
|
|
171
|
+
* with a well-formed empty list. `registerTools` returns the names it did register, which is how
|
|
172
|
+
* an application observes that.
|
|
173
|
+
*/
|
|
174
|
+
readonly permits: (requirements: readonly SecurityRequirement[]) => boolean;
|
|
175
|
+
/**
|
|
176
|
+
* The caller's context, or `null` when there is none to establish.
|
|
177
|
+
*
|
|
178
|
+
* `authentication` is what the DOCUMENT says and only that, decided at generation time:
|
|
179
|
+
*
|
|
180
|
+
* - `"none"`: no alternative asks for anything, `@useAuth(NoAuth)` or no auth at all;
|
|
181
|
+
* - `"optional"`: an anonymous alternative sits beside a real one, `NoAuth | BearerAuth`, so a
|
|
182
|
+
* caller presenting a credential is recognised and one presenting none is still admitted;
|
|
183
|
+
* - `"required"`: every alternative asks for something.
|
|
184
|
+
*
|
|
185
|
+
* Nothing richer is offered, because nothing richer is published -- the sibling emitter once
|
|
186
|
+
* invented a distinction from whether a path had parameters, which no OpenAPI keyword expresses.
|
|
187
|
+
*/
|
|
188
|
+
readonly context: (context: ServerContext, authentication: "none" | "optional" | "required") => C | null;
|
|
189
|
+
/** The result when {@link context} returns `null`. The application's to decide. */
|
|
190
|
+
readonly noContext: (context: ServerContext) => CallToolResult;
|
|
191
|
+
}
|