@agent-proxy-kit/puraroute-mcp-server 0.0.0-stage → 0.1.1

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 (33) hide show
  1. package/DISCLOSURE +1 -0
  2. package/LICENSE +15 -0
  3. package/README.md +19 -2
  4. package/bundle/adapter-policy.json +11 -0
  5. package/bundle/brand.json +9 -0
  6. package/bundle/bundle-manifest.json +34 -0
  7. package/bundle/business-openapi.yaml +3518 -0
  8. package/bundle/run-mcp.mjs +38 -0
  9. package/bundle/setup.mjs +34 -0
  10. package/bundle/skill/puraroute-proxy/SKILL.md +26 -0
  11. package/bundle/skill/puraroute-proxy/examples/connection-cases.md +33 -0
  12. package/bundle/skill/puraroute-proxy/references/connection-formats.md +40 -0
  13. package/bundle/skill/puraroute-proxy/references/products.md +9 -0
  14. package/bundle/skill/puraroute-proxy/references/setup.md +19 -0
  15. package/bundle/skill/puraroute-proxy/references/troubleshooting.md +17 -0
  16. package/bundle/skill/puraroute-proxy/references/verified-contract.md +34 -0
  17. package/bundle/skill/puraroute-proxy/references/workflows.md +33 -0
  18. package/bundle/skill/puraroute-proxy/references/zh-CN.md +13 -0
  19. package/bundle/tool-constraints.json +28 -0
  20. package/dist/brand.js +40 -0
  21. package/dist/catalog/input-validator.js +40 -0
  22. package/dist/catalog/load.js +137 -0
  23. package/dist/catalog/operations.js +83 -0
  24. package/dist/catalog/project-schema.js +192 -0
  25. package/dist/cli.js +135 -0
  26. package/dist/config.js +32 -0
  27. package/dist/runtime/execute-operation.js +161 -0
  28. package/dist/runtime/register-openapi.js +12 -0
  29. package/dist/runtime/register-tools.js +4 -0
  30. package/dist/runtime/response.js +44 -0
  31. package/dist/runtime/secrets.js +5 -0
  32. package/dist/transports/stdio.js +14 -0
  33. package/package.json +33 -4
@@ -0,0 +1,83 @@
1
+ export const HTTP_METHODS = [
2
+ "get",
3
+ "post",
4
+ "put",
5
+ "patch",
6
+ "delete",
7
+ ];
8
+ const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
9
+ ? value
10
+ : {};
11
+ export function collectOperations(document, basePath, version) {
12
+ if (document.webhooks !== undefined)
13
+ throw new Error("webhooks are unsupported in the customer business catalog");
14
+ const paths = obj(document.paths);
15
+ const operations = [];
16
+ const seen = new Set();
17
+ for (const [path, pathValue] of Object.entries(paths)) {
18
+ const item = obj(pathValue);
19
+ for (const method of ["head", "options", "trace"]) {
20
+ if (item[method] !== undefined)
21
+ throw new Error(`${method.toUpperCase()} ${path}: unsupported HTTP method`);
22
+ }
23
+ if (item.servers !== undefined)
24
+ throw new Error(`${path}: path-level servers are unsupported`);
25
+ const effective = `${basePath}${path}`;
26
+ if (!effective.startsWith("/gin/open-api/v1/") ||
27
+ path.includes("..") ||
28
+ path.includes("?") ||
29
+ path.includes("#")) {
30
+ throw new Error(`${path}: operation path is outside /gin/open-api/v1/`);
31
+ }
32
+ for (const method of HTTP_METHODS) {
33
+ if (item[method] === undefined)
34
+ continue;
35
+ const raw = obj(item[method]);
36
+ const label = `${method.toUpperCase()} ${path}`;
37
+ if (raw.servers !== undefined)
38
+ throw new Error(`${label}: operation-level servers are unsupported`);
39
+ if (raw.callbacks !== undefined)
40
+ throw new Error(`${label}: callbacks are unsupported`);
41
+ const operationId = raw.operationId;
42
+ if (typeof operationId !== "string" ||
43
+ !/^[A-Za-z][A-Za-z0-9_-]*$/.test(operationId)) {
44
+ throw new Error(`${label}: missing or invalid operationId`);
45
+ }
46
+ if (seen.has(operationId))
47
+ throw new Error(`${operationId}: duplicate operationId`);
48
+ seen.add(operationId);
49
+ const summary = raw.summary;
50
+ if (typeof summary !== "string" || !summary.trim())
51
+ throw new Error(`${operationId}: missing useful summary`);
52
+ const parameters = [
53
+ ...(Array.isArray(item.parameters) ? item.parameters : []),
54
+ ...(Array.isArray(raw.parameters) ? raw.parameters : []),
55
+ ];
56
+ const responses = obj(raw.responses);
57
+ const schemaOf = (value) => obj(obj(obj(value).content)["application/json"]).schema;
58
+ const success = Object.entries(responses).find(([status, value]) => /^2\d\d$/.test(status) && schemaOf(value) !== undefined);
59
+ const successResponseSchema = success
60
+ ? obj(schemaOf(success[1]))
61
+ : undefined;
62
+ operations.push({
63
+ operationId,
64
+ method,
65
+ pathTemplate: effective,
66
+ summary,
67
+ description: typeof raw.description === "string" ? raw.description : "",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {},
71
+ additionalProperties: false,
72
+ },
73
+ ...(successResponseSchema ? { successResponseSchema } : {}),
74
+ parameters: parameters.map(obj),
75
+ ...(raw.requestBody !== undefined
76
+ ? { requestBody: obj(raw.requestBody) }
77
+ : {}),
78
+ openapiVersion: version,
79
+ });
80
+ }
81
+ }
82
+ return operations.sort((a, b) => a.operationId.localeCompare(b.operationId, "en"));
83
+ }
@@ -0,0 +1,192 @@
1
+ import { newInputValidator } from "./input-validator.js";
2
+ const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
3
+ ? value
4
+ : {};
5
+ export const FORBIDDEN_HEADERS = new Set([
6
+ "x-api-key",
7
+ "authorization",
8
+ "host",
9
+ "cookie",
10
+ "traceparent",
11
+ "tracestate",
12
+ "x-request-id",
13
+ "x-trace-id",
14
+ "content-length",
15
+ "connection",
16
+ "transfer-encoding",
17
+ "proxy-authorization",
18
+ ]);
19
+ export function ignoredOptionalTransportHeader(parameter) {
20
+ return (parameter.in === "header" &&
21
+ typeof parameter.name === "string" &&
22
+ parameter.name.toLowerCase() === "x-trace-id" &&
23
+ parameter.required !== true);
24
+ }
25
+ function projectSchema(value, version, label) {
26
+ if (!value || typeof value !== "object" || Array.isArray(value))
27
+ throw new Error(`${label}: schema must be an object`);
28
+ const source = value;
29
+ if ("$ref" in source)
30
+ throw new Error(`${label}: unresolved $ref`);
31
+ if (source.format === "binary" || source.format === "byte")
32
+ throw new Error(`${label}: file upload schema is unsupported`);
33
+ const output = {};
34
+ for (const [key, item] of Object.entries(source)) {
35
+ if (key === "nullable" && version.startsWith("3.0."))
36
+ continue;
37
+ if (["properties", "patternProperties", "$defs", "definitions"].includes(key)) {
38
+ output[key] = Object.fromEntries(Object.entries(obj(item)).map(([name, child]) => [
39
+ name,
40
+ projectSchema(child, version, `${label}.${name}`),
41
+ ]));
42
+ }
43
+ else if (["items", "additionalProperties", "not"].includes(key) &&
44
+ typeof item === "object" &&
45
+ item !== null) {
46
+ output[key] = projectSchema(item, version, `${label}.${key}`);
47
+ }
48
+ else if (["allOf", "anyOf", "oneOf"].includes(key) &&
49
+ Array.isArray(item)) {
50
+ output[key] = item.map((child, index) => projectSchema(child, version, `${label}.${key}[${index}]`));
51
+ }
52
+ else if (version.startsWith("3.0.") &&
53
+ (key === "exclusiveMinimum" || key === "exclusiveMaximum") &&
54
+ typeof item === "boolean") {
55
+ if (item) {
56
+ const bound = key === "exclusiveMinimum" ? source.minimum : source.maximum;
57
+ if (typeof bound !== "number")
58
+ throw new Error(`${label}: ${key} needs a numeric bound`);
59
+ output[key] = bound;
60
+ }
61
+ }
62
+ else if (version.startsWith("3.0.") &&
63
+ ((key === "minimum" && source.exclusiveMinimum === true) ||
64
+ (key === "maximum" && source.exclusiveMaximum === true))) {
65
+ continue;
66
+ }
67
+ else {
68
+ output[key] = item;
69
+ }
70
+ }
71
+ if (version.startsWith("3.0.") && source.nullable === true) {
72
+ if (typeof output.type === "string")
73
+ output.type = [output.type, "null"];
74
+ else
75
+ output.anyOf = [
76
+ ...(Array.isArray(output.anyOf)
77
+ ? output.anyOf
78
+ : [
79
+ Object.fromEntries(Object.entries(output).filter(([key]) => key !== "anyOf")),
80
+ ]),
81
+ { type: "null" },
82
+ ];
83
+ }
84
+ return output;
85
+ }
86
+ function groupSchema(properties, required) {
87
+ return {
88
+ type: "object",
89
+ properties,
90
+ additionalProperties: false,
91
+ ...(required.length ? { required } : {}),
92
+ };
93
+ }
94
+ export function projectOperationSchema(operation) {
95
+ const groups = {
96
+ path: {},
97
+ query: {},
98
+ headers: {},
99
+ };
100
+ const required = {
101
+ path: [],
102
+ query: [],
103
+ headers: [],
104
+ };
105
+ const seen = new Set();
106
+ for (const parameter of operation.parameters) {
107
+ const name = parameter.name;
108
+ const location = parameter.in;
109
+ if (typeof name !== "string" ||
110
+ !name ||
111
+ !["path", "query", "header"].includes(String(location))) {
112
+ throw new Error(`${operation.operationId}: unsupported ${String(location)} parameter`);
113
+ }
114
+ const place = location === "header" ? "headers" : location;
115
+ const key = `${location}:${name.toLowerCase()}`;
116
+ if (seen.has(key))
117
+ throw new Error(`${operation.operationId}: duplicate ${location} parameter ${name}`);
118
+ seen.add(key);
119
+ if (location === "header" && FORBIDDEN_HEADERS.has(name.toLowerCase())) {
120
+ if (ignoredOptionalTransportHeader(parameter))
121
+ continue;
122
+ throw new Error(`${operation.operationId}: forbidden header ${name}`);
123
+ }
124
+ if (parameter.content !== undefined)
125
+ throw new Error(`${operation.operationId}: parameter content is unsupported`);
126
+ const style = parameter.style ?? (location === "query" ? "form" : "simple");
127
+ if (style !== (location === "query" ? "form" : "simple") ||
128
+ parameter.allowReserved === true) {
129
+ throw new Error(`${operation.operationId}: unsupported parameter style for ${name}`);
130
+ }
131
+ if (location !== "query" && parameter.explode === true)
132
+ throw new Error(`${operation.operationId}: unsupported explode for ${name}`);
133
+ const schema = projectSchema(parameter.schema, operation.openapiVersion, `${operation.operationId}.${name}`);
134
+ const type = schema.type;
135
+ const scalar = typeof type === "string" &&
136
+ ["string", "integer", "number", "boolean"].includes(type);
137
+ const array = location === "query" &&
138
+ type === "array" &&
139
+ ["string", "integer", "number", "boolean"].includes(String(obj(schema.items).type));
140
+ const nullableScalar = Array.isArray(type) &&
141
+ type.length === 2 &&
142
+ type.includes("null") &&
143
+ type.some((part) => ["string", "integer", "number", "boolean"].includes(String(part)));
144
+ if (!scalar && !array && !nullableScalar)
145
+ throw new Error(`${operation.operationId}: unsupported parameter schema for ${name}`);
146
+ groups[place][name] = schema;
147
+ if (parameter.required === true || location === "path")
148
+ required[place].push(name);
149
+ if (location === "path" && !operation.pathTemplate.includes(`{${name}}`))
150
+ throw new Error(`${operation.operationId}: unmatched path parameter ${name}`);
151
+ }
152
+ for (const match of operation.pathTemplate.matchAll(/\{([^}]+)\}/g)) {
153
+ if (!Object.hasOwn(groups.path, match[1]))
154
+ throw new Error(`${operation.operationId}: missing path parameter ${match[1]}`);
155
+ }
156
+ const properties = {};
157
+ const topRequired = [];
158
+ for (const place of ["path", "query", "headers"]) {
159
+ if (Object.keys(groups[place]).length) {
160
+ properties[place] = groupSchema(groups[place], required[place]);
161
+ if (required[place].length)
162
+ topRequired.push(place);
163
+ }
164
+ }
165
+ if (operation.requestBody) {
166
+ const content = obj(operation.requestBody.content);
167
+ if (Object.keys(content).length !== 1 || !("application/json" in content))
168
+ throw new Error(`${operation.operationId}: only application/json request bodies are supported`);
169
+ properties.body = projectSchema(obj(content["application/json"]).schema, operation.openapiVersion, `${operation.operationId}.body`);
170
+ if (operation.requestBody.required === true)
171
+ topRequired.push("body");
172
+ }
173
+ const schema = groupSchema(properties, topRequired);
174
+ if (operation.inputConstraints)
175
+ schema.allOf = [operation.inputConstraints];
176
+ return schema;
177
+ }
178
+ export function projectCatalogSchemas(operations) {
179
+ const schemas = operations.map(projectOperationSchema);
180
+ const validator = newInputValidator();
181
+ schemas.forEach((schema, index) => {
182
+ try {
183
+ validator.compile(schema);
184
+ }
185
+ catch {
186
+ throw new Error(`${operations[index].operationId}: projected JSON Schema is invalid`);
187
+ }
188
+ });
189
+ operations.forEach((operation, index) => {
190
+ operation.inputSchema = schemas[index];
191
+ });
192
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { readBrandProfile } from "./brand.js";
7
+ import { loadCatalog } from "./catalog/load.js";
8
+ import { projectCatalogSchemas } from "./catalog/project-schema.js";
9
+ import { readConfig } from "./config.js";
10
+ import { startStdio } from "./transports/stdio.js";
11
+ const ownDir = dirname(fileURLToPath(import.meta.url));
12
+ async function bundleContract(bundleDir) {
13
+ const found = [];
14
+ for (const name of ["business-openapi.yaml", "business-openapi.json"]) {
15
+ const candidate = join(bundleDir, name);
16
+ try {
17
+ await access(candidate);
18
+ found.push(candidate);
19
+ }
20
+ catch {
21
+ /* try next supported format */
22
+ }
23
+ }
24
+ if (found.length !== 1)
25
+ throw new Error("Bundle requires exactly one business OpenAPI file");
26
+ return found[0];
27
+ }
28
+ async function verifyBundleManifest(bundleDir, contractSha256, operations, brandVersion, runtimeVersion) {
29
+ let manifest;
30
+ try {
31
+ manifest = JSON.parse(await readFile(join(bundleDir, "bundle-manifest.json"), "utf8"));
32
+ }
33
+ catch {
34
+ throw new Error("Missing or invalid bundle-manifest.json");
35
+ }
36
+ if (!manifest ||
37
+ typeof manifest !== "object" ||
38
+ Array.isArray(manifest) ||
39
+ Object.keys(manifest).sort().join(",") !==
40
+ [
41
+ "bundleVersion",
42
+ "contractSha256",
43
+ "operations",
44
+ "runtimeVersion",
45
+ "toolSchemasSha256",
46
+ ]
47
+ .sort()
48
+ .join(","))
49
+ throw new Error("Invalid bundle-manifest.json");
50
+ const toolHash = createHash("sha256")
51
+ .update(JSON.stringify(operations.map((op) => [op.operationId, op.inputSchema])))
52
+ .digest("hex");
53
+ if (manifest.bundleVersion !== brandVersion ||
54
+ manifest.runtimeVersion !== runtimeVersion ||
55
+ manifest.contractSha256 !== contractSha256 ||
56
+ manifest.toolSchemasSha256 !== toolHash ||
57
+ JSON.stringify(manifest.operations) !==
58
+ JSON.stringify(operations.map((op) => op.operationId)))
59
+ throw new Error("Bundle manifest mismatch");
60
+ }
61
+ async function readPolicy(contractPath) {
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(await readFile(join(dirname(contractPath), "adapter-policy.json"), "utf8"));
65
+ }
66
+ catch {
67
+ throw new Error("Missing or invalid adapter-policy.json beside the contract");
68
+ }
69
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
70
+ throw new Error("Invalid adapter policy");
71
+ const policy = parsed;
72
+ const envelope = policy.envelope;
73
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope))
74
+ throw new Error("Invalid adapter policy envelope");
75
+ const fields = envelope;
76
+ if (!["code", "message", "data", "next"].every((name) => typeof fields[name] === "string" && fields[name].length > 0))
77
+ throw new Error("Invalid adapter policy envelope fields");
78
+ if (!Array.isArray(policy.successCodes) ||
79
+ policy.successCodes.length === 0 ||
80
+ !policy.successCodes.every((value) => typeof value === "string" || typeof value === "number"))
81
+ throw new Error("Invalid adapter policy success codes");
82
+ if (policy.successHttpStatuses !== undefined &&
83
+ (!Array.isArray(policy.successHttpStatuses) ||
84
+ !policy.successHttpStatuses.every((value) => Number.isInteger(value) && value >= 200 && value < 300)))
85
+ throw new Error("Invalid adapter policy success HTTP statuses");
86
+ if (policy.requiredResponseFields !== undefined &&
87
+ (!Array.isArray(policy.requiredResponseFields) ||
88
+ !policy.requiredResponseFields.every((value) => typeof value === "string")))
89
+ throw new Error("Invalid adapter policy required response fields");
90
+ return parsed;
91
+ }
92
+ async function main() {
93
+ const check = process.argv[2] === "--check-contract";
94
+ if (check) {
95
+ const path = process.argv[3];
96
+ if (!path || !isAbsolute(path) || process.argv.length !== 4)
97
+ throw new Error("--check-contract requires one absolute local file path");
98
+ const catalog = await loadCatalog(path);
99
+ projectCatalogSchemas(catalog.operations);
100
+ process.stdout.write(`OpenAPI ${catalog.openapiVersion}\nSHA-256 ${catalog.contractSha256}\nOperations ${catalog.operations.length}\n${catalog.operations.map((op) => op.operationId).join("\n")}\n`);
101
+ return;
102
+ }
103
+ if (process.argv.length !== 2)
104
+ throw new Error("Unknown CLI argument");
105
+ const bundleDir = process.env.PROXY_AGENT_BUNDLE_DIR;
106
+ if (!bundleDir || !isAbsolute(bundleDir))
107
+ throw new Error("PROXY_AGENT_BUNDLE_DIR must be an absolute directory");
108
+ const brand = await readBrandProfile(bundleDir);
109
+ const contractPath = await bundleContract(bundleDir);
110
+ let catalog;
111
+ try {
112
+ catalog = await loadCatalog(contractPath);
113
+ projectCatalogSchemas(catalog.operations);
114
+ }
115
+ catch {
116
+ throw new Error("Invalid business OpenAPI in bundle");
117
+ }
118
+ const adapterPolicy = await readPolicy(contractPath);
119
+ const config = readConfig(process.env, contractPath, adapterPolicy);
120
+ let packageJson;
121
+ try {
122
+ packageJson = JSON.parse(await readFile(join(ownDir, "..", "package.json"), "utf8"));
123
+ }
124
+ catch {
125
+ throw new Error("Invalid runtime installation");
126
+ }
127
+ await verifyBundleManifest(bundleDir, catalog.contractSha256, catalog.operations, brand.version, packageJson.runtimeVersion ?? packageJson.version);
128
+ startStdio({ catalog, config }, packageJson.version, brand.serverName);
129
+ }
130
+ main().catch((error) => {
131
+ const message = error instanceof Error ? error.message : "Unexpected startup failure";
132
+ const key = process.env.PROXY_AGENT_API_KEY;
133
+ process.stderr.write(`${key ? message.replaceAll(key, "[REDACTED]") : message}\n`);
134
+ process.exitCode = 1;
135
+ });
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ export function readConfig(env, contractPath, adapterPolicy) {
2
+ const originText = env.PROXY_AGENT_API_ORIGIN;
3
+ if (!originText)
4
+ throw new Error("Missing PROXY_AGENT_API_ORIGIN");
5
+ let apiOrigin;
6
+ try {
7
+ apiOrigin = new URL(originText);
8
+ }
9
+ catch {
10
+ throw new Error("Invalid API origin");
11
+ }
12
+ if (apiOrigin.protocol !== "https:")
13
+ throw new Error("API origin must use HTTPS");
14
+ if (apiOrigin.pathname !== "/" ||
15
+ apiOrigin.search ||
16
+ apiOrigin.hash ||
17
+ apiOrigin.username ||
18
+ apiOrigin.password) {
19
+ throw new Error("API origin must contain only a scheme and host");
20
+ }
21
+ const apiKey = env.PROXY_AGENT_API_KEY;
22
+ if (!apiKey?.trim())
23
+ throw new Error("Missing PROXY_AGENT_API_KEY");
24
+ const timeoutText = env.PROXY_AGENT_REQUEST_TIMEOUT_MS;
25
+ const requestTimeoutMs = timeoutText === undefined ? 15_000 : Number(timeoutText);
26
+ if (!Number.isSafeInteger(requestTimeoutMs) ||
27
+ requestTimeoutMs < 100 ||
28
+ requestTimeoutMs > 120_000) {
29
+ throw new Error("Invalid request timeout");
30
+ }
31
+ return { apiOrigin, apiKey, contractPath, requestTimeoutMs, adapterPolicy };
32
+ }
@@ -0,0 +1,161 @@
1
+ import { LosslessNumber, parse } from "lossless-json";
2
+ import { newInputValidator } from "../catalog/input-validator.js";
3
+ import { FORBIDDEN_HEADERS, ignoredOptionalTransportHeader, projectOperationSchema, } from "../catalog/project-schema.js";
4
+ import { adaptResponse, toolError } from "./response.js";
5
+ import { safeIdentifier } from "./secrets.js";
6
+ const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
7
+ ? value
8
+ : {};
9
+ const writeMethod = (method) => method !== "get";
10
+ function ordinaryJson(value) {
11
+ if (value instanceof LosslessNumber) {
12
+ const numeric = Number(value.value);
13
+ return /^-?(?:0|[1-9]\d*)$/.test(value.value) &&
14
+ Number.isSafeInteger(numeric)
15
+ ? numeric
16
+ : value.value;
17
+ }
18
+ if (Array.isArray(value))
19
+ return value.map(ordinaryJson);
20
+ if (value !== null && typeof value === "object") {
21
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
22
+ key,
23
+ ordinaryJson(child),
24
+ ]));
25
+ }
26
+ return value;
27
+ }
28
+ function redactAccountKey(value, apiKey) {
29
+ if (typeof value === "string")
30
+ return value.replaceAll(apiKey, "[REDACTED]");
31
+ if (Array.isArray(value))
32
+ return value.map((item) => redactAccountKey(item, apiKey));
33
+ if (value !== null && typeof value === "object") {
34
+ return Object.fromEntries(Object.entries(value)
35
+ .filter(([name]) => !["api_key", "apikey", "x-api-key"].includes(name.toLowerCase()))
36
+ .map(([name, item]) => [name, redactAccountKey(item, apiKey)]));
37
+ }
38
+ return value;
39
+ }
40
+ function validatedArgs(operation, args) {
41
+ const validator = newInputValidator();
42
+ const valid = validator.compile(projectOperationSchema(operation));
43
+ return valid(args) ? args : undefined;
44
+ }
45
+ function requestUrl(operation, args, config) {
46
+ if (!operation.pathTemplate.startsWith("/gin/open-api/v1/") ||
47
+ operation.pathTemplate.includes("..") ||
48
+ operation.pathTemplate.includes("\\") ||
49
+ operation.pathTemplate.includes("//")) {
50
+ throw new Error("Unsafe approved operation path");
51
+ }
52
+ const pathArgs = obj(args.path);
53
+ const path = operation.pathTemplate.replace(/\{([^}]+)\}/g, (_match, name) => {
54
+ const value = pathArgs[name];
55
+ if (value === undefined ||
56
+ value === null ||
57
+ value === "." ||
58
+ value === ".." ||
59
+ String(value).includes("/") ||
60
+ String(value).includes("\\"))
61
+ throw new Error("Unsafe path argument");
62
+ return encodeURIComponent(String(value));
63
+ });
64
+ if (path.includes("{") || path.includes("}"))
65
+ throw new Error("Unresolved path template");
66
+ const url = new URL(path, config.apiOrigin);
67
+ if (url.origin !== config.apiOrigin.origin ||
68
+ !url.pathname.startsWith("/gin/open-api/v1/"))
69
+ throw new Error("Unsafe target URL");
70
+ const query = obj(args.query);
71
+ for (const parameter of operation.parameters.filter((item) => item.in === "query")) {
72
+ const name = String(parameter.name);
73
+ const value = query[name];
74
+ if (value === undefined || value === null)
75
+ continue;
76
+ if (Array.isArray(value)) {
77
+ if (parameter.explode === false)
78
+ url.searchParams.append(name, value.map(String).join(","));
79
+ else
80
+ value.forEach((item) => url.searchParams.append(name, String(item)));
81
+ }
82
+ else
83
+ url.searchParams.append(name, String(value));
84
+ }
85
+ return url;
86
+ }
87
+ export async function executeOperation(operation, args, config) {
88
+ const isHttps = config.apiOrigin.protocol === "https:";
89
+ const isLoopback = config.apiOrigin.protocol === "http:" &&
90
+ ["127.0.0.1", "localhost", "[::1]"].includes(config.apiOrigin.hostname);
91
+ if (!isHttps && !isLoopback)
92
+ return toolError("configuration_error", "API origin must use HTTPS");
93
+ const input = validatedArgs(operation, args);
94
+ if (!input)
95
+ return toolError("input_error", `Invalid arguments for ${operation.operationId}`);
96
+ let url;
97
+ try {
98
+ url = requestUrl(operation, input, config);
99
+ }
100
+ catch {
101
+ return toolError("input_error", `Unsafe URL composition for ${operation.operationId}`);
102
+ }
103
+ const headers = new Headers({
104
+ "X-API-Key": config.apiKey,
105
+ Accept: "application/json",
106
+ });
107
+ for (const parameter of operation.parameters.filter((item) => item.in === "header")) {
108
+ if (ignoredOptionalTransportHeader(parameter))
109
+ continue;
110
+ const name = String(parameter.name);
111
+ if (FORBIDDEN_HEADERS.has(name.toLowerCase()))
112
+ return toolError("input_error", `Forbidden header for ${operation.operationId}`);
113
+ const value = obj(input.headers)[name];
114
+ if (value !== undefined && value !== null)
115
+ headers.set(name, String(value));
116
+ }
117
+ let body;
118
+ if (input.body !== undefined) {
119
+ body = JSON.stringify(input.body);
120
+ headers.set("Content-Type", "application/json");
121
+ }
122
+ const candidateRequestNo = safeIdentifier(obj(input.body).request_no);
123
+ const requestNo = candidateRequestNo === config.apiKey ? undefined : candidateRequestNo;
124
+ let response;
125
+ try {
126
+ response = await fetch(url, {
127
+ method: operation.method.toUpperCase(),
128
+ headers,
129
+ ...(body === undefined ? {} : { body }),
130
+ redirect: "manual",
131
+ signal: AbortSignal.timeout(config.requestTimeoutMs),
132
+ });
133
+ }
134
+ catch (error) {
135
+ const timeout = error instanceof Error &&
136
+ (error.name === "TimeoutError" || error.name === "AbortError");
137
+ if (timeout && writeMethod(operation.method)) {
138
+ return toolError("uncertain_write_result", `The ${operation.operationId} request timed out after submission. Check orders or resources with a read-only lookup before another write.${requestNo ? ` Request number: ${requestNo}.` : ""}`, requestNo
139
+ ? { operationId: operation.operationId, requestNo }
140
+ : { operationId: operation.operationId });
141
+ }
142
+ return toolError(timeout ? "timeout" : "network_error", timeout
143
+ ? `Request timed out for ${operation.operationId}`
144
+ : `Network request failed for ${operation.operationId}`);
145
+ }
146
+ const traceId = response.headers.get("x-request-id") ??
147
+ response.headers.get("x-trace-id") ??
148
+ undefined;
149
+ if (response.status >= 300 && response.status < 400)
150
+ return toolError("redirect_blocked", `Redirect blocked for ${operation.operationId}`);
151
+ if (!response.ok)
152
+ return adaptResponse(response.status, undefined, traceId, config.adapterPolicy);
153
+ let parsed;
154
+ try {
155
+ parsed = redactAccountKey(ordinaryJson(parse(await response.text())), config.apiKey);
156
+ }
157
+ catch {
158
+ return toolError("malformed_json", `Invalid JSON response for ${operation.operationId}`);
159
+ }
160
+ return adaptResponse(response.status, parsed, traceId, config.adapterPolicy);
161
+ }
@@ -0,0 +1,12 @@
1
+ import { fromJsonSchema, } from "@modelcontextprotocol/server";
2
+ import { projectCatalogSchemas } from "../catalog/project-schema.js";
3
+ import { executeOperation } from "./execute-operation.js";
4
+ export function registerOpenApiTools(server, catalog, config, executor = executeOperation) {
5
+ projectCatalogSchemas(catalog.operations);
6
+ for (const operation of catalog.operations) {
7
+ server.registerTool(operation.operationId, {
8
+ description: operation.description || operation.summary,
9
+ inputSchema: fromJsonSchema(operation.inputSchema),
10
+ }, async (args) => executor(operation, args, config));
11
+ }
12
+ }
@@ -0,0 +1,4 @@
1
+ import { registerOpenApiTools } from "./register-openapi.js";
2
+ export function registerTools(server, context) {
3
+ registerOpenApiTools(server, context.catalog, context.config);
4
+ }
@@ -0,0 +1,44 @@
1
+ import { safeIdentifier } from "./secrets.js";
2
+ export function toolError(kind, message, details = {}) {
3
+ const structuredContent = { error: kind, message, ...details };
4
+ return {
5
+ isError: true,
6
+ structuredContent,
7
+ content: [{ type: "text", text: `${kind}: ${message}` }],
8
+ };
9
+ }
10
+ export function adaptResponse(status, body, traceId, policy) {
11
+ const safeTrace = safeIdentifier(traceId);
12
+ if (status < 200 || status >= 300)
13
+ return toolError("http_error", `HTTP ${status}`, safeTrace ? { traceId: safeTrace } : {});
14
+ if (policy.successHttpStatuses &&
15
+ !policy.successHttpStatuses.includes(status))
16
+ return toolError("http_error", `Unexpected HTTP ${status}`);
17
+ if (!body || typeof body !== "object" || Array.isArray(body))
18
+ return toolError("malformed_json", "Expected a JSON object");
19
+ const record = body;
20
+ if (policy.requiredResponseFields?.some((field) => !Object.hasOwn(record, field)))
21
+ return toolError("malformed_json", "Missing required response envelope field");
22
+ const code = record[policy.envelope.code];
23
+ const structuredContent = {
24
+ ...record,
25
+ ...(safeTrace ? { traceId: safeTrace } : {}),
26
+ };
27
+ const success = policy.successCodes.some((item) => item === code);
28
+ if (!success) {
29
+ return {
30
+ isError: true,
31
+ structuredContent,
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: `business_error: code ${String(code ?? "missing")}`,
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ return {
41
+ structuredContent,
42
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
43
+ };
44
+ }
@@ -0,0 +1,5 @@
1
+ export function safeIdentifier(value) {
2
+ if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value))
3
+ return undefined;
4
+ return value;
5
+ }