@knitli/openapi-mcp 0.0.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.
Files changed (116) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +362 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +249 -0
  6. package/dist/compile.d.ts +14 -0
  7. package/dist/compile.js +132 -0
  8. package/dist/compiler.d.ts +13 -0
  9. package/dist/compiler.js +7 -0
  10. package/dist/conformance/fixtures.d.ts +24 -0
  11. package/dist/conformance/fixtures.js +119 -0
  12. package/dist/conformance/index.d.ts +2 -0
  13. package/dist/conformance/index.js +2 -0
  14. package/dist/conformance/runtime-suite.d.ts +31 -0
  15. package/dist/conformance/runtime-suite.js +343 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/load.d.ts +31 -0
  19. package/dist/load.js +20 -0
  20. package/dist/operations.d.ts +27 -0
  21. package/dist/operations.js +231 -0
  22. package/dist/permissions.d.ts +31 -0
  23. package/dist/permissions.js +89 -0
  24. package/dist/release/compile-release.d.ts +33 -0
  25. package/dist/release/compile-release.js +1620 -0
  26. package/dist/release/load-v4.d.ts +29 -0
  27. package/dist/release/load-v4.js +297 -0
  28. package/dist/release/manifest-builder.d.ts +19 -0
  29. package/dist/release/manifest-builder.js +28 -0
  30. package/dist/release/publish.d.ts +45 -0
  31. package/dist/release/publish.js +346 -0
  32. package/dist/release/reference-map.d.ts +53 -0
  33. package/dist/release/reference-map.js +347 -0
  34. package/dist/release/schema-v4.d.ts +5 -0
  35. package/dist/release/schema-v4.js +91 -0
  36. package/dist/runtime/action-permit.d.ts +11 -0
  37. package/dist/runtime/action-permit.js +1 -0
  38. package/dist/runtime/classify.d.ts +16 -0
  39. package/dist/runtime/classify.js +279 -0
  40. package/dist/runtime/credential-binding.d.ts +3 -0
  41. package/dist/runtime/credential-binding.js +101 -0
  42. package/dist/runtime/digest.d.ts +8 -0
  43. package/dist/runtime/digest.js +80 -0
  44. package/dist/runtime/errors.d.ts +14 -0
  45. package/dist/runtime/errors.js +15 -0
  46. package/dist/runtime/index.d.ts +20 -0
  47. package/dist/runtime/index.js +14 -0
  48. package/dist/runtime/manifest.d.ts +33 -0
  49. package/dist/runtime/manifest.js +489 -0
  50. package/dist/runtime/prepared-call.d.ts +39 -0
  51. package/dist/runtime/prepared-call.js +566 -0
  52. package/dist/runtime/references.d.ts +7 -0
  53. package/dist/runtime/references.js +197 -0
  54. package/dist/runtime/runtime.d.ts +25 -0
  55. package/dist/runtime/runtime.js +1272 -0
  56. package/dist/runtime/schema-keywords.d.ts +2 -0
  57. package/dist/runtime/schema-keywords.js +30 -0
  58. package/dist/runtime/schema-resolver.d.ts +10 -0
  59. package/dist/runtime/schema-resolver.js +237 -0
  60. package/dist/runtime/serialize.d.ts +12 -0
  61. package/dist/runtime/serialize.js +877 -0
  62. package/dist/runtime/store.d.ts +52 -0
  63. package/dist/runtime/store.js +582 -0
  64. package/dist/runtime/strict-json.d.ts +19 -0
  65. package/dist/runtime/strict-json.js +539 -0
  66. package/dist/runtime/types.d.ts +457 -0
  67. package/dist/runtime/types.js +1 -0
  68. package/dist/runtime/verify-record.d.ts +6 -0
  69. package/dist/runtime/verify-record.js +492 -0
  70. package/dist/runtime/versions.d.ts +63 -0
  71. package/dist/runtime/versions.js +86 -0
  72. package/dist/safety.d.ts +28 -0
  73. package/dist/safety.js +60 -0
  74. package/dist/schema.d.ts +5 -0
  75. package/dist/schema.js +52 -0
  76. package/dist/schemas.d.ts +9 -0
  77. package/dist/schemas.js +16 -0
  78. package/dist/sign.d.ts +20 -0
  79. package/dist/sign.js +55 -0
  80. package/dist/sqlite/auth.d.ts +33 -0
  81. package/dist/sqlite/auth.js +973 -0
  82. package/dist/sqlite/catalog-store.d.ts +20 -0
  83. package/dist/sqlite/catalog-store.js +370 -0
  84. package/dist/sqlite/destination-guard.d.ts +24 -0
  85. package/dist/sqlite/destination-guard.js +140 -0
  86. package/dist/sqlite/generation-store.d.ts +12 -0
  87. package/dist/sqlite/generation-store.js +721 -0
  88. package/dist/sqlite/guarded-fetch.d.ts +24 -0
  89. package/dist/sqlite/guarded-fetch.js +626 -0
  90. package/dist/sqlite/index.d.ts +5 -0
  91. package/dist/sqlite/index.js +5 -0
  92. package/dist/sqlite/oauth-resource.d.ts +2 -0
  93. package/dist/sqlite/oauth-resource.js +15 -0
  94. package/dist/sqlite/release-completion.d.ts +5 -0
  95. package/dist/sqlite/release-completion.js +80 -0
  96. package/dist/stdio/action-broker.d.ts +15 -0
  97. package/dist/stdio/action-broker.js +136 -0
  98. package/dist/stdio/authorizer.d.ts +30 -0
  99. package/dist/stdio/authorizer.js +484 -0
  100. package/dist/stdio/config.d.ts +77 -0
  101. package/dist/stdio/config.js +144 -0
  102. package/dist/stdio/exact-policy.d.ts +41 -0
  103. package/dist/stdio/exact-policy.js +361 -0
  104. package/dist/stdio/index.d.ts +6 -0
  105. package/dist/stdio/index.js +148 -0
  106. package/dist/stdio/render.d.ts +8 -0
  107. package/dist/stdio/render.js +253 -0
  108. package/dist/stdio/request-state.d.ts +18 -0
  109. package/dist/stdio/request-state.js +79 -0
  110. package/dist/stdio/search.d.ts +10 -0
  111. package/dist/stdio/search.js +76 -0
  112. package/dist/stdio/server.d.ts +29 -0
  113. package/dist/stdio/server.js +309 -0
  114. package/dist/types.d.ts +32 -0
  115. package/dist/types.js +1 -0
  116. package/package.json +131 -0
@@ -0,0 +1,148 @@
1
+ import { serveStdio, } from "@modelcontextprotocol/server/stdio";
2
+ import { admitExecutableRelease, createOpenApiRuntime, verifyExecutableRelease, } from "../runtime/runtime.js";
3
+ import { resolveRuntimeLimits } from "../runtime/versions.js";
4
+ import { createCredentialProvider, } from "../sqlite/auth.js";
5
+ import { SqliteCatalogStore } from "../sqlite/catalog-store.js";
6
+ import { FileGenerationStore } from "../sqlite/generation-store.js";
7
+ import { createLocalDispatchBoundary } from "../sqlite/guarded-fetch.js";
8
+ import { parseOpenApiStdioConfig } from "./config.js";
9
+ import { compileExactPolicy } from "./exact-policy.js";
10
+ import { createStdioSearchRuntime } from "./search.js";
11
+ import { createOpenApiMcpServer, createStdioActionAuthorizer, } from "./server.js";
12
+ export { parseOpenApiStdioConfig } from "./config.js";
13
+ export { createOpenApiMcpServer, } from "./server.js";
14
+ /** Owns process-local credentials/catalogs independently of disposable SDK probes. */
15
+ export async function serveOpenApiStdio(input) {
16
+ const config = parseOpenApiStdioConfig(input);
17
+ const stores = [];
18
+ const providers = [];
19
+ const boundaries = [];
20
+ let sdk;
21
+ let closing;
22
+ const onTerminate = () => {
23
+ void close().catch(() => {
24
+ process.stderr.write("OpenAPI stdio cleanup failed\n");
25
+ process.exitCode = 1;
26
+ });
27
+ };
28
+ const close = () => (closing ??= (async () => {
29
+ process.off("SIGTERM", onTerminate);
30
+ process.off("SIGINT", onTerminate);
31
+ process.stdin.off("end", onTerminate);
32
+ const results = await Promise.allSettled([
33
+ ...boundaries.map((entry) => entry.close()),
34
+ ...providers.map((entry) => entry.close()),
35
+ ...(sdk ? [sdk.close()] : []),
36
+ ]);
37
+ for (const store of stores)
38
+ store.close();
39
+ if (results.some((entry) => entry.status === "rejected"))
40
+ throw new Error("OpenAPI stdio cleanup failed");
41
+ })());
42
+ try {
43
+ const generations = new FileGenerationStore(config.generationStatePath);
44
+ const limits = resolveRuntimeLimits(config.limits);
45
+ const admitted = [];
46
+ for (const entry of config.catalogs) {
47
+ const store = new SqliteCatalogStore(entry.path, { limits });
48
+ stores.push(store);
49
+ const verified = await verifyExecutableRelease({ store, trust: config.trust, generations, limits }, entry.catalogId, entry.releaseId);
50
+ if (verified.manifest.catalogId !== entry.catalogId ||
51
+ verified.manifest.releaseId !== entry.releaseId)
52
+ throw new Error("Catalog identity mismatch");
53
+ admitted.push({ config: entry, store, verified });
54
+ }
55
+ const templates = await Promise.all((config.exactPolicies ?? []).map((entry) => compileExactPolicy(entry)));
56
+ // Reject invalid releases and policies before advancing any catalog.
57
+ // GenerationStore provides per-catalog CAS, not a cross-catalog transaction.
58
+ for (const entry of admitted)
59
+ await admitExecutableRelease({ store: entry.store, trust: config.trust, generations, limits }, entry.verified);
60
+ const authorizer = createStdioActionAuthorizer(templates);
61
+ const routes = [];
62
+ for (const profile of config.profiles) {
63
+ const catalogs = admitted.filter((entry) => entry.config.profileId === profile.profileId);
64
+ if (!catalogs.length)
65
+ continue;
66
+ const credentials = await createCredentialProvider(profile, {
67
+ manifestOrigins: [
68
+ ...new Set(catalogs.flatMap((entry) => entry.verified.manifest.allowedOrigins)),
69
+ ],
70
+ });
71
+ providers.push(credentials);
72
+ const boundary = createLocalDispatchBoundary(authorizer, {
73
+ profile,
74
+ limits,
75
+ async allowsManifestOrigin(context) {
76
+ const entry = catalogs.find((entry) => entry.verified.manifest.catalogId === context.catalogId &&
77
+ entry.verified.manifest.releaseId === context.releaseId &&
78
+ entry.verified.manifestDigest === context.manifestDigest);
79
+ if (!entry ||
80
+ !config.allowedOrigins.includes(context.origin) ||
81
+ !entry.verified.manifest.allowedOrigins.includes(context.origin))
82
+ return false;
83
+ try {
84
+ const current = await generations.get(entry.verified.manifest.catalogId, entry.verified.manifest.issuer);
85
+ return (current?.activeGeneration ===
86
+ entry.verified.manifest.generation &&
87
+ current.activeManifestDigest === entry.verified.manifestDigest);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ },
93
+ });
94
+ boundaries.push(boundary);
95
+ for (const entry of catalogs)
96
+ routes.push({
97
+ catalogId: entry.config.catalogId,
98
+ releaseId: entry.config.releaseId,
99
+ apiNamespaces: [
100
+ ...new Set(Object.keys(entry.verified.manifest.records)
101
+ .filter((id) => id.startsWith("operation:"))
102
+ .map((id) => id.split(":")[1])),
103
+ ],
104
+ credentials,
105
+ boundary,
106
+ runtime: createOpenApiRuntime({
107
+ store: entry.store,
108
+ trust: config.trust,
109
+ generations,
110
+ destinationPolicy: {
111
+ async allows(origin) {
112
+ return (config.allowedOrigins.includes(origin) &&
113
+ profile.allowedOrigins.includes(origin));
114
+ },
115
+ },
116
+ credentialBinding: credentials.bindingResolver,
117
+ paginationTokenCodec: boundary.paginationTokenCodec,
118
+ limits,
119
+ }),
120
+ });
121
+ }
122
+ sdk = serveStdio(() => createOpenApiMcpServer({
123
+ routes,
124
+ searchRuntime: createStdioSearchRuntime(admitted.map((entry) => ({
125
+ catalogId: entry.config.catalogId,
126
+ releaseId: entry.config.releaseId,
127
+ apiNamespaces: routes.find((route) => route.catalogId === entry.config.catalogId)?.apiNamespaces ?? [],
128
+ store: entry.store,
129
+ })), { trust: config.trust, generations, limits }),
130
+ limits,
131
+ authorizer,
132
+ maxSearchResults: limits.maxSearchResults,
133
+ defaultSearchResults: limits.defaultSearchResults,
134
+ }), {
135
+ onerror() {
136
+ process.stderr.write("OpenAPI stdio protocol error\n");
137
+ },
138
+ });
139
+ process.once("SIGTERM", onTerminate);
140
+ process.once("SIGINT", onTerminate);
141
+ process.stdin.once("end", onTerminate);
142
+ return Object.freeze({ close });
143
+ }
144
+ catch {
145
+ await close().catch(() => { });
146
+ throw new Error("OpenAPI stdio startup failed");
147
+ }
148
+ }
@@ -0,0 +1,8 @@
1
+ import type { CredentialAuthorizationBinding, PreparedCall, SafeApprovalPresentation, Sha256 } from "../runtime/types.ts";
2
+ interface PolicyActivationInput {
3
+ readonly policyDigest: Sha256;
4
+ readonly expiresAt: number;
5
+ readonly template: unknown;
6
+ }
7
+ export declare function renderApproval(call: PreparedCall, binding: CredentialAuthorizationBinding, activation?: PolicyActivationInput): SafeApprovalPresentation;
8
+ export {};
@@ -0,0 +1,253 @@
1
+ import { OpenApiMcpError } from "../runtime/errors.js";
2
+ import { canonicalJsonBounded } from "../runtime/strict-json.js";
3
+ const MAX_PRESENTATION_BYTES = 16 * 1024;
4
+ const MAX_ARGUMENT_SUMMARY_BYTES = 2_048;
5
+ const MAX_DESCRIPTION_BYTES = 512;
6
+ const MAX_TEMPLATE_BYTES = 64 * 1024;
7
+ const TRUNCATION_MARKER = "TRUNCATED";
8
+ function denied() {
9
+ return new OpenApiMcpError("ACTION_DENIED", "Approval details cannot be displayed safely");
10
+ }
11
+ function bytes(value) {
12
+ return Buffer.byteLength(value, "utf8");
13
+ }
14
+ function escapedCodePoint(character) {
15
+ const codePoint = character.codePointAt(0);
16
+ if (codePoint === undefined)
17
+ return "";
18
+ const isControl = codePoint <= 0x1f ||
19
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
20
+ codePoint === 0x2028 ||
21
+ codePoint === 0x2029;
22
+ const isDirectionControl = codePoint === 0x061c ||
23
+ (codePoint >= 0x200b && codePoint <= 0x200f) ||
24
+ (codePoint >= 0x202a && codePoint <= 0x202e) ||
25
+ (codePoint >= 0x2066 && codePoint <= 0x2069);
26
+ const isAsciiPunctuationOrSpace = codePoint <= 0x7e &&
27
+ !((codePoint >= 0x30 && codePoint <= 0x39) ||
28
+ (codePoint >= 0x41 && codePoint <= 0x5a) ||
29
+ (codePoint >= 0x61 && codePoint <= 0x7a));
30
+ if (!isControl && !isDirectionControl && !isAsciiPunctuationOrSpace) {
31
+ return character;
32
+ }
33
+ if (codePoint <= 0xffff) {
34
+ return `\\u${codePoint.toString(16).toUpperCase().padStart(4, "0")}`;
35
+ }
36
+ return `\\u{${codePoint.toString(16).toUpperCase()}}`;
37
+ }
38
+ /** A display encoding, not an HTML or Markdown escaping context. */
39
+ function escapeValue(value, maxBytes) {
40
+ let rendered = "";
41
+ for (const character of value) {
42
+ const next = escapedCodePoint(character);
43
+ if (maxBytes !== undefined &&
44
+ bytes(rendered) + bytes(next) + bytes(TRUNCATION_MARKER) > maxBytes) {
45
+ return rendered + TRUNCATION_MARKER;
46
+ }
47
+ rendered += next;
48
+ }
49
+ return rendered;
50
+ }
51
+ function quoted(value) {
52
+ return `"${escapeValue(value)}"`;
53
+ }
54
+ function quotedDescription(value) {
55
+ return `"${escapeValue(value, MAX_DESCRIPTION_BYTES)}"`;
56
+ }
57
+ function cardinalityText(cardinality) {
58
+ return cardinality.kind === "bounded"
59
+ ? `bounded (${cardinality.maxAffected})`
60
+ : cardinality.kind;
61
+ }
62
+ function dataProperties(value) {
63
+ const entries = [];
64
+ for (const key of Object.keys(value).sort()) {
65
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
66
+ if (descriptor !== undefined && "value" in descriptor) {
67
+ entries.push([key, descriptor.value]);
68
+ }
69
+ }
70
+ return entries;
71
+ }
72
+ function valueType(value) {
73
+ if (value === null)
74
+ return "null";
75
+ if (Array.isArray(value))
76
+ return "array";
77
+ return typeof value;
78
+ }
79
+ function scalarDescription(value) {
80
+ if (typeof value === "string")
81
+ return quotedDescription(value);
82
+ if (value === null ||
83
+ typeof value === "boolean" ||
84
+ typeof value === "number") {
85
+ return quotedDescription(String(value));
86
+ }
87
+ if (Array.isArray(value))
88
+ return `array (${value.length} items)`;
89
+ if (typeof value === "object") {
90
+ return `object (${dataProperties(value).length} keys)`;
91
+ }
92
+ return valueType(value);
93
+ }
94
+ function bodyStructure(body) {
95
+ const lines = [];
96
+ const seen = new WeakSet();
97
+ function visit(value, path, depth) {
98
+ if (lines.length >= 128)
99
+ return;
100
+ if (Array.isArray(value)) {
101
+ if (seen.has(value)) {
102
+ lines.push(`${path}: cyclic array`);
103
+ return;
104
+ }
105
+ seen.add(value);
106
+ const types = [...new Set(value.slice(0, 64).map(valueType))].sort();
107
+ lines.push(`${path}: array (${value.length} items; observed types: ${types.join(", ") || "none"})`);
108
+ if (depth < 4 && value.length > 0)
109
+ visit(value[0], `${path}[]`, depth + 1);
110
+ return;
111
+ }
112
+ if (typeof value === "object" && value !== null) {
113
+ if (seen.has(value)) {
114
+ lines.push(`${path}: cyclic object`);
115
+ return;
116
+ }
117
+ seen.add(value);
118
+ const properties = dataProperties(value);
119
+ const keys = properties.map(([key]) => quotedDescription(key)).join(", ");
120
+ lines.push(`${path}: object (${properties.length} keys: ${keys || "none"})`);
121
+ if (depth < 4) {
122
+ for (const [key, child] of properties) {
123
+ if (Array.isArray(child) ||
124
+ (typeof child === "object" && child !== null)) {
125
+ visit(child, `${path}.${escapeValue(key, MAX_DESCRIPTION_BYTES)}`, depth + 1);
126
+ }
127
+ }
128
+ }
129
+ return;
130
+ }
131
+ lines.push(`${path}: ${valueType(value)}`);
132
+ }
133
+ visit(body, "Body structure", 0);
134
+ return lines;
135
+ }
136
+ function boundedLines(lines, maximum) {
137
+ let result = "";
138
+ for (const line of lines) {
139
+ const candidate = result.length === 0 ? line : `${result}\n${line}`;
140
+ if (bytes(candidate) <= maximum) {
141
+ result = candidate;
142
+ continue;
143
+ }
144
+ const marker = result.length === 0 ? TRUNCATION_MARKER : `\n${TRUNCATION_MARKER}`;
145
+ if (bytes(result) + bytes(marker) <= maximum)
146
+ result += marker;
147
+ break;
148
+ }
149
+ return result;
150
+ }
151
+ function argumentSummary(call) {
152
+ const lines = [];
153
+ for (const [section, value] of dataProperties(call.normalizedArguments)) {
154
+ if (section === "body") {
155
+ lines.push(...bodyStructure(value));
156
+ continue;
157
+ }
158
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
159
+ const properties = dataProperties(value);
160
+ lines.push(`${escapeValue(section, MAX_DESCRIPTION_BYTES)}: ${properties.length} entries`);
161
+ for (const [key, child] of properties) {
162
+ lines.push(` ${quotedDescription(key)}: ${scalarDescription(child)}`);
163
+ }
164
+ continue;
165
+ }
166
+ lines.push(`${escapeValue(section, MAX_DESCRIPTION_BYTES)}: ${scalarDescription(value)}`);
167
+ }
168
+ return boundedLines(lines.length === 0 ? ["none"] : lines, MAX_ARGUMENT_SUMMARY_BYTES);
169
+ }
170
+ function canonicalTemplate(template) {
171
+ try {
172
+ return canonicalJsonBounded(template, {
173
+ maxBytes: MAX_TEMPLATE_BYTES,
174
+ maxDepth: 32,
175
+ maxNodes: 4_096,
176
+ });
177
+ }
178
+ catch {
179
+ throw denied();
180
+ }
181
+ }
182
+ function frozenCardinality(value) {
183
+ return value.kind === "bounded"
184
+ ? Object.freeze({ kind: value.kind, maxAffected: value.maxAffected })
185
+ : Object.freeze({ kind: value.kind });
186
+ }
187
+ export function renderApproval(call, binding, activation) {
188
+ try {
189
+ const credentialProfile = escapeValue(binding.profileId);
190
+ const audience = escapeValue(binding.audience ?? "not specified");
191
+ const scopes = Object.freeze(binding.scopes.map((scope) => escapeValue(scope)));
192
+ const normalizedArguments = argumentSummary(call);
193
+ const policyActivation = activation
194
+ ? Object.freeze({
195
+ policyDigest: activation.policyDigest,
196
+ expiresAt: activation.expiresAt,
197
+ })
198
+ : undefined;
199
+ const lines = [
200
+ "An external action requires approval.",
201
+ `Catalog: ${quoted(call.catalogId)}`,
202
+ `Release: ${quoted(call.releaseId)}`,
203
+ `Operation: ${quoted(call.operationId)}`,
204
+ `Method: ${quoted(call.method)}`,
205
+ `Origin: ${quoted(call.origin)}`,
206
+ `Relative URL: ${quoted(call.relativeUrl)}`,
207
+ `Action kind: ${quoted(call.actionKind ?? "unknown")}`,
208
+ `Cardinality: ${quoted(cardinalityText(call.cardinality ?? { kind: "unknown" }))}`,
209
+ `Credential profile: "${credentialProfile}"`,
210
+ `Credential audience: "${audience}"`,
211
+ `Credential scopes (${scopes.length}): ${scopes.map((scope) => `"${scope}"`).join(", ") || "none"}`,
212
+ `Operation digest: ${quoted(call.operationDigest)}`,
213
+ `Manifest digest: ${quoted(call.manifestDigest)}`,
214
+ `Credential profile digest: ${quoted(call.credentialProfileDigest)}`,
215
+ `Reserved slots digest: ${quoted(call.reservedSlotsDigest)}`,
216
+ `Prepared call digest: ${quoted(call.preparedCallDigest)}`,
217
+ `Credential binding digest: ${quoted(binding.bindingDigest)}`,
218
+ "Arguments summary:",
219
+ normalizedArguments,
220
+ ];
221
+ if (activation !== undefined) {
222
+ const template = escapeValue(canonicalTemplate(activation.template));
223
+ lines.push("POLICY ACTIVATION WARNING: approving this request also activates reusable standing authority for matching calls.", `Policy digest: ${quoted(activation.policyDigest)}`, `Policy expiry (Unix milliseconds): ${quoted(String(activation.expiresAt))}`, "Complete canonical template constraints:", `"${template}"`);
224
+ }
225
+ const message = lines.join("\n");
226
+ if (bytes(message) > MAX_PRESENTATION_BYTES)
227
+ throw denied();
228
+ return Object.freeze({
229
+ message,
230
+ credentialProfile,
231
+ audience,
232
+ scopes,
233
+ credentialBindingDigest: binding.bindingDigest,
234
+ ...(policyActivation === undefined ? {} : { policyActivation }),
235
+ catalogId: escapeValue(call.catalogId),
236
+ releaseId: escapeValue(call.releaseId),
237
+ operationId: escapeValue(call.operationId),
238
+ method: call.method,
239
+ origin: escapeValue(call.origin),
240
+ relativeUrl: escapeValue(call.relativeUrl),
241
+ actionKind: call.actionKind ?? "unknown",
242
+ cardinality: frozenCardinality(call.cardinality ?? { kind: "unknown" }),
243
+ normalizedArguments,
244
+ preparedCallDigest: call.preparedCallDigest,
245
+ });
246
+ }
247
+ catch (error) {
248
+ if (error instanceof OpenApiMcpError && error.code === "ACTION_DENIED") {
249
+ throw error;
250
+ }
251
+ throw denied();
252
+ }
253
+ }
@@ -0,0 +1,18 @@
1
+ import { type ServerContext } from "@modelcontextprotocol/server";
2
+ import type { Sha256, VerifiedActionRequestState } from "../runtime/types.ts";
3
+ export interface PendingRequestStatePayload {
4
+ readonly version: 1;
5
+ readonly pendingId: string;
6
+ readonly callDigest: Sha256;
7
+ readonly credentialBindingDigest: Sha256;
8
+ readonly expiresAt: number;
9
+ }
10
+ export declare function createVerifiedRequestStateCodec(options: {
11
+ readonly key: Uint8Array;
12
+ readonly now: () => number;
13
+ readonly isPending: (payload: PendingRequestStatePayload) => boolean;
14
+ }): Readonly<{
15
+ mint(payload: PendingRequestStatePayload): Promise<string>;
16
+ verify(state: string, context?: ServerContext): Promise<VerifiedActionRequestState>;
17
+ read(capability: VerifiedActionRequestState): PendingRequestStatePayload | undefined;
18
+ }>;
@@ -0,0 +1,79 @@
1
+ import { createRequestStateCodec, } from "@modelcontextprotocol/server";
2
+ const digestPattern = /^[0-9a-f]{64}$/;
3
+ const idPattern = /^[0-9a-f]{32}$/;
4
+ function decodePayload(value) {
5
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6
+ throw new Error("malformed");
7
+ }
8
+ const prototype = Object.getPrototypeOf(value);
9
+ if (prototype !== Object.prototype && prototype !== null) {
10
+ throw new Error("malformed");
11
+ }
12
+ const keys = Reflect.ownKeys(value);
13
+ const expected = [
14
+ "callDigest",
15
+ "credentialBindingDigest",
16
+ "expiresAt",
17
+ "pendingId",
18
+ "version",
19
+ ];
20
+ if (keys.some((key) => typeof key !== "string") ||
21
+ keys.sort().join(",") !== expected.join(",")) {
22
+ throw new Error("malformed");
23
+ }
24
+ const payload = Object.create(null);
25
+ for (const key of keys) {
26
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
27
+ if (descriptor === undefined ||
28
+ !("value" in descriptor) ||
29
+ !descriptor.enumerable) {
30
+ throw new Error("malformed");
31
+ }
32
+ payload[key] = descriptor.value;
33
+ }
34
+ if (payload.version !== 1 ||
35
+ typeof payload.pendingId !== "string" ||
36
+ !idPattern.test(payload.pendingId) ||
37
+ typeof payload.callDigest !== "string" ||
38
+ !digestPattern.test(payload.callDigest) ||
39
+ typeof payload.credentialBindingDigest !== "string" ||
40
+ !digestPattern.test(payload.credentialBindingDigest) ||
41
+ typeof payload.expiresAt !== "number" ||
42
+ !Number.isSafeInteger(payload.expiresAt)) {
43
+ throw new Error("malformed");
44
+ }
45
+ return Object.freeze({
46
+ version: 1,
47
+ pendingId: payload.pendingId,
48
+ callDigest: payload.callDigest,
49
+ credentialBindingDigest: payload.credentialBindingDigest,
50
+ expiresAt: payload.expiresAt,
51
+ });
52
+ }
53
+ export function createVerifiedRequestStateCodec(options) {
54
+ if (!(options.key instanceof Uint8Array) || options.key.byteLength < 32) {
55
+ throw new RangeError("HMAC key is too short");
56
+ }
57
+ const codec = createRequestStateCodec({
58
+ key: new Uint8Array(options.key),
59
+ ttlSeconds: 120,
60
+ });
61
+ const registered = new WeakMap();
62
+ return Object.freeze({
63
+ async mint(payload) {
64
+ return codec.mint(decodePayload(payload));
65
+ },
66
+ async verify(state, context) {
67
+ const decoded = decodePayload(await codec.verify(state, context));
68
+ if (options.now() >= decoded.expiresAt || !options.isPending(decoded)) {
69
+ throw new Error("expired");
70
+ }
71
+ const capability = Object.freeze({});
72
+ registered.set(capability, decoded);
73
+ return capability;
74
+ },
75
+ read(capability) {
76
+ return registered.get(capability);
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,10 @@
1
+ import { type OpenApiRuntimeOptions } from "../runtime/runtime.ts";
2
+ import type { CatalogStore, OpenApiRuntime } from "../runtime/types.ts";
3
+ export interface StdioSearchCatalog {
4
+ readonly catalogId: string;
5
+ readonly releaseId: string;
6
+ readonly apiNamespaces: readonly string[];
7
+ readonly store: CatalogStore;
8
+ }
9
+ /** One semantic search, including one shared proof/work/byte budget across catalogs. */
10
+ export declare function createStdioSearchRuntime(catalogs: readonly StdioSearchCatalog[], options: Omit<OpenApiRuntimeOptions, "store">): Pick<OpenApiRuntime, "search">;
@@ -0,0 +1,76 @@
1
+ import { OpenApiMcpError } from "../runtime/errors.js";
2
+ import { createRuntimeWithCandidateLookup, } from "../runtime/runtime.js";
3
+ import { resolveRuntimeLimits } from "../runtime/versions.js";
4
+ /** One semantic search, including one shared proof/work/byte budget across catalogs. */
5
+ export function createStdioSearchRuntime(catalogs, options) {
6
+ const ordered = [...catalogs].sort((a, b) => {
7
+ const left = `${a.catalogId}\0${a.releaseId}`;
8
+ const right = `${b.catalogId}\0${b.releaseId}`;
9
+ return left < right ? -1 : left > right ? 1 : 0;
10
+ });
11
+ const limits = resolveRuntimeLimits(options.limits);
12
+ const stores = new Map(ordered.map((entry) => [
13
+ `${entry.catalogId}\0${entry.releaseId}`,
14
+ entry.store,
15
+ ]));
16
+ const storeFor = (catalogId, releaseId) => {
17
+ const store = stores.get(`${catalogId}\0${releaseId}`);
18
+ if (!store)
19
+ throw new OpenApiMcpError("RECORD_NOT_ADMITTED");
20
+ return store;
21
+ };
22
+ return {
23
+ async search(input) {
24
+ const matching = ordered.filter((entry) => input.api === undefined || entry.apiNamespaces.includes(input.api));
25
+ const store = {
26
+ getManifest: (catalog, release) => storeFor(catalog, release).getManifest(catalog, release),
27
+ getOperation: (catalog, release, id) => storeFor(catalog, release).getOperation(catalog, release, id),
28
+ getSchemas: (catalog, release, ids) => storeFor(catalog, release).getSchemas(catalog, release, ids),
29
+ async searchCandidates() {
30
+ throw new OpenApiMcpError("UPSTREAM_ERROR");
31
+ },
32
+ };
33
+ return createRuntimeWithCandidateLookup({ ...options, limits, store }, async (query, tryChargeSource) => {
34
+ const batches = [];
35
+ let remaining = query.limit;
36
+ let limited = false;
37
+ // Source calls share the proof budget, including queries with no matches.
38
+ // One total candidate allowance bounds hydration across all sources.
39
+ // FTS ranks are local; interleave equal local ranks in stable catalog order.
40
+ for (const [index, entry] of matching.entries()) {
41
+ if (remaining === 0)
42
+ break;
43
+ if (!tryChargeSource()) {
44
+ limited = true;
45
+ break;
46
+ }
47
+ const limit = Math.min(limits.maxSearchResults, Math.ceil(remaining / (matching.length - index)));
48
+ const rows = await entry.store.searchCandidates({
49
+ ...query,
50
+ limit,
51
+ });
52
+ if (rows.length > limit ||
53
+ rows.some((row) => row.catalogId !== entry.catalogId ||
54
+ row.releaseId !== entry.releaseId))
55
+ throw new OpenApiMcpError("RECORD_NOT_ADMITTED");
56
+ remaining -= rows.length;
57
+ batches.push(rows);
58
+ }
59
+ const candidates = [];
60
+ for (let rank = 0; candidates.length < query.limit; rank++) {
61
+ let found = false;
62
+ for (const batch of batches) {
63
+ const candidate = batch[rank];
64
+ if (candidate) {
65
+ candidates.push(candidate);
66
+ found = true;
67
+ }
68
+ }
69
+ if (!found)
70
+ break;
71
+ }
72
+ return { candidates, limited };
73
+ }).search(input);
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,29 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import type { CredentialProvider, OpenApiRuntime } from "../runtime/types.ts";
3
+ import { type RuntimeLimits } from "../runtime/versions.ts";
4
+ import type { createLocalDispatchBoundary } from "../sqlite/guarded-fetch.ts";
5
+ import { type StatefulActionAuthorizer } from "./authorizer.ts";
6
+ import type { CompiledExactPolicy } from "./exact-policy.ts";
7
+ type Boundary = ReturnType<typeof createLocalDispatchBoundary>;
8
+ /** One authorizer across factory instances preserves opaque continuation ownership. */
9
+ export declare function createStdioActionAuthorizer(templates?: readonly CompiledExactPolicy[]): StatefulActionAuthorizer;
10
+ export interface OpenApiServerRoute {
11
+ readonly catalogId: string;
12
+ readonly releaseId: string;
13
+ /** API names from the host's configured, verified manifest inventory. */
14
+ readonly apiNamespaces: readonly string[];
15
+ readonly runtime: OpenApiRuntime;
16
+ readonly credentials: CredentialProvider;
17
+ readonly boundary: Boundary;
18
+ }
19
+ export interface OpenApiMcpServerOptions {
20
+ readonly routes: readonly OpenApiServerRoute[];
21
+ /** Host-owned cross-catalog search with one application budget. */
22
+ readonly searchRuntime: Pick<OpenApiRuntime, "search">;
23
+ readonly authorizer: StatefulActionAuthorizer;
24
+ readonly limits?: Partial<RuntimeLimits>;
25
+ readonly maxSearchResults?: number;
26
+ readonly defaultSearchResults?: number;
27
+ }
28
+ export declare function createOpenApiMcpServer(options: OpenApiMcpServerOptions): McpServer;
29
+ export {};