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.
@@ -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
+ }