caplets 0.7.0 → 0.8.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/README.md +107 -9
- package/dist/index.js +518 -51
- package/package.json +1 -1
- package/schemas/caplet.schema.json +274 -0
- package/schemas/caplets-config.schema.json +302 -0
package/dist/index.js
CHANGED
|
@@ -180,7 +180,7 @@ const allowsEval = /* @__PURE__ */ cached(() => {
|
|
|
180
180
|
return false;
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
|
-
function isPlainObject$
|
|
183
|
+
function isPlainObject$5(o) {
|
|
184
184
|
if (isObject(o) === false) return false;
|
|
185
185
|
const ctor = o.constructor;
|
|
186
186
|
if (ctor === void 0) return true;
|
|
@@ -191,7 +191,7 @@ function isPlainObject$4(o) {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
function shallowClone(o) {
|
|
194
|
-
if (isPlainObject$
|
|
194
|
+
if (isPlainObject$5(o)) return { ...o };
|
|
195
195
|
if (Array.isArray(o)) return [...o];
|
|
196
196
|
if (o instanceof Map) return new Map(o);
|
|
197
197
|
if (o instanceof Set) return new Set(o);
|
|
@@ -274,7 +274,7 @@ function omit(schema, mask) {
|
|
|
274
274
|
}));
|
|
275
275
|
}
|
|
276
276
|
function extend(schema, shape) {
|
|
277
|
-
if (!isPlainObject$
|
|
277
|
+
if (!isPlainObject$5(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
278
278
|
const checks = schema._zod.def.checks;
|
|
279
279
|
if (checks && checks.length > 0) {
|
|
280
280
|
const existingShape = schema._zod.def.shape;
|
|
@@ -290,7 +290,7 @@ function extend(schema, shape) {
|
|
|
290
290
|
} }));
|
|
291
291
|
}
|
|
292
292
|
function safeExtend(schema, shape) {
|
|
293
|
-
if (!isPlainObject$
|
|
293
|
+
if (!isPlainObject$5(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
294
294
|
return clone(schema, mergeDefs(schema._zod.def, { get shape() {
|
|
295
295
|
const _shape = {
|
|
296
296
|
...schema._zod.def.shape,
|
|
@@ -1904,7 +1904,7 @@ function mergeValues$1(a, b) {
|
|
|
1904
1904
|
valid: true,
|
|
1905
1905
|
data: a
|
|
1906
1906
|
};
|
|
1907
|
-
if (isPlainObject$
|
|
1907
|
+
if (isPlainObject$5(a) && isPlainObject$5(b)) {
|
|
1908
1908
|
const bKeys = Object.keys(b);
|
|
1909
1909
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
1910
1910
|
const newObj = {
|
|
@@ -1980,7 +1980,7 @@ const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
1980
1980
|
$ZodType.init(inst, def);
|
|
1981
1981
|
inst._zod.parse = (payload, ctx) => {
|
|
1982
1982
|
const input = payload.value;
|
|
1983
|
-
if (!isPlainObject$
|
|
1983
|
+
if (!isPlainObject$5(input)) {
|
|
1984
1984
|
payload.issues.push({
|
|
1985
1985
|
expected: "record",
|
|
1986
1986
|
code: "invalid_type",
|
|
@@ -9619,7 +9619,7 @@ const { program, createCommand, createArgument, createOption, CommanderError, In
|
|
|
9619
9619
|
})))(), 1)).default;
|
|
9620
9620
|
//#endregion
|
|
9621
9621
|
//#region package.json
|
|
9622
|
-
var version = "0.
|
|
9622
|
+
var version = "0.8.0";
|
|
9623
9623
|
//#endregion
|
|
9624
9624
|
//#region node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
|
|
9625
9625
|
let crypto;
|
|
@@ -18680,6 +18680,7 @@ function matter(file, options) {
|
|
|
18680
18680
|
//#region src/config/validation.ts
|
|
18681
18681
|
const SERVER_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
18682
18682
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
18683
|
+
const HTTP_BASE_URL_PATTERN = /^(?![a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*@)[^?#]*$/;
|
|
18683
18684
|
const FORBIDDEN_HEADERS = new Set([
|
|
18684
18685
|
"accept",
|
|
18685
18686
|
"authorization",
|
|
@@ -18697,6 +18698,16 @@ const FORBIDDEN_HEADERS = new Set([
|
|
|
18697
18698
|
"transfer-encoding",
|
|
18698
18699
|
"upgrade"
|
|
18699
18700
|
]);
|
|
18701
|
+
function validateHttpActionHeaders(headers, ctx, path) {
|
|
18702
|
+
for (const headerName of Object.keys(headers)) {
|
|
18703
|
+
const normalized = headerName.toLowerCase();
|
|
18704
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || FORBIDDEN_HEADERS.has(normalized)) ctx.addIssue({
|
|
18705
|
+
code: "custom",
|
|
18706
|
+
path: [...path, headerName],
|
|
18707
|
+
message: `header ${headerName} is not allowed`
|
|
18708
|
+
});
|
|
18709
|
+
}
|
|
18710
|
+
}
|
|
18700
18711
|
function isAllowedRemoteUrl(value) {
|
|
18701
18712
|
let url;
|
|
18702
18713
|
try {
|
|
@@ -18712,6 +18723,23 @@ function isAllowedRemoteUrl(value) {
|
|
|
18712
18723
|
"::1"
|
|
18713
18724
|
].includes(url.hostname);
|
|
18714
18725
|
}
|
|
18726
|
+
function isAllowedHttpBaseUrl(value) {
|
|
18727
|
+
let url;
|
|
18728
|
+
try {
|
|
18729
|
+
url = new URL(value);
|
|
18730
|
+
} catch {
|
|
18731
|
+
return false;
|
|
18732
|
+
}
|
|
18733
|
+
return isAllowedRemoteUrl(value) && !url.username && !url.password && !url.search && !url.hash;
|
|
18734
|
+
}
|
|
18735
|
+
function isUrl(value) {
|
|
18736
|
+
try {
|
|
18737
|
+
new URL(value);
|
|
18738
|
+
return true;
|
|
18739
|
+
} catch {
|
|
18740
|
+
return false;
|
|
18741
|
+
}
|
|
18742
|
+
}
|
|
18715
18743
|
//#endregion
|
|
18716
18744
|
//#region src/caplet-files.ts
|
|
18717
18745
|
const MAX_CAPLET_FILE_BYTES = 128 * 1024;
|
|
@@ -18917,6 +18945,52 @@ const capletGraphQlEndpointSchema = object$1({
|
|
|
18917
18945
|
});
|
|
18918
18946
|
validateEndpointAuthHeaders$1(endpoint.auth, ctx);
|
|
18919
18947
|
});
|
|
18948
|
+
const httpScalarMappingSchema$1 = record(string(), union([
|
|
18949
|
+
string(),
|
|
18950
|
+
number$1(),
|
|
18951
|
+
boolean()
|
|
18952
|
+
]));
|
|
18953
|
+
const capletHttpActionSchema = object$1({
|
|
18954
|
+
method: _enum([
|
|
18955
|
+
"GET",
|
|
18956
|
+
"POST",
|
|
18957
|
+
"PUT",
|
|
18958
|
+
"PATCH",
|
|
18959
|
+
"DELETE"
|
|
18960
|
+
]).describe("HTTP method used for this action."),
|
|
18961
|
+
path: string().min(1).regex(/^\//, "HTTP action path must start with /").describe("URL path appended to the HTTP API baseUrl.").refine((value) => !value.startsWith("//"), "HTTP action path must not start with //").refine((value) => !isUrl(value), "HTTP action path must be a URL path, not a URL"),
|
|
18962
|
+
description: string().min(1).optional().describe("Action capability description."),
|
|
18963
|
+
inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
|
|
18964
|
+
query: httpScalarMappingSchema$1.optional().describe("Query parameter mapping."),
|
|
18965
|
+
headers: httpScalarMappingSchema$1.optional().describe("Request header mapping."),
|
|
18966
|
+
jsonBody: unknown().optional().describe("JSON request body mapping.")
|
|
18967
|
+
}).strict().superRefine((action, ctx) => {
|
|
18968
|
+
if (action.method === "GET" && action.jsonBody !== void 0) ctx.addIssue({
|
|
18969
|
+
code: "custom",
|
|
18970
|
+
path: ["jsonBody"],
|
|
18971
|
+
message: "HTTP GET actions must not define jsonBody"
|
|
18972
|
+
});
|
|
18973
|
+
});
|
|
18974
|
+
const capletHttpApiSchema = object$1({
|
|
18975
|
+
baseUrl: string().min(1).regex(HTTP_BASE_URL_PATTERN, "HTTP API baseUrl must not include credentials, query, or fragment").describe("Base URL for HTTP action requests."),
|
|
18976
|
+
auth: capletEndpointAuthSchema.describe("Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs."),
|
|
18977
|
+
actions: record(string().regex(SERVER_ID_PATTERN), capletHttpActionSchema).refine((actions) => Object.keys(actions).length > 0, "HTTP API must define at least one action").describe("Configured HTTP actions keyed by stable tool name."),
|
|
18978
|
+
requestTimeoutMs: number$1().int().positive().optional().describe("Timeout in milliseconds for HTTP action requests."),
|
|
18979
|
+
maxResponseBytes: number$1().int().positive().optional().describe("Maximum HTTP action response body bytes to read."),
|
|
18980
|
+
disabled: boolean().optional().describe("When true, omit this Caplet from discovery.")
|
|
18981
|
+
}).strict().superRefine((api, ctx) => {
|
|
18982
|
+
if (api.baseUrl && !hasEnvReference$1(api.baseUrl) && !isAllowedHttpBaseUrl(api.baseUrl)) ctx.addIssue({
|
|
18983
|
+
code: "custom",
|
|
18984
|
+
path: ["baseUrl"],
|
|
18985
|
+
message: "HTTP API baseUrl must use https except loopback development urls and must not include credentials, query, or fragment"
|
|
18986
|
+
});
|
|
18987
|
+
validateEndpointAuthHeaders$1(api.auth, ctx);
|
|
18988
|
+
for (const [actionName, action] of Object.entries(api.actions)) if (action.headers) validateHttpActionHeaders(action.headers, ctx, [
|
|
18989
|
+
"actions",
|
|
18990
|
+
actionName,
|
|
18991
|
+
"headers"
|
|
18992
|
+
]);
|
|
18993
|
+
});
|
|
18920
18994
|
const capletFileSchema = object$1({
|
|
18921
18995
|
$schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
|
|
18922
18996
|
name: string().trim().min(1).max(80).describe("Human-readable Caplet display name."),
|
|
@@ -18924,11 +18998,12 @@ const capletFileSchema = object$1({
|
|
|
18924
18998
|
tags: array(string().trim().min(1).max(80)).optional().describe("Optional tags for grouping or searching Caplets."),
|
|
18925
18999
|
mcpServer: capletMcpServerSchema.describe("MCP server backend configuration for this Caplet.").optional(),
|
|
18926
19000
|
openapiEndpoint: capletOpenApiEndpointSchema.describe("OpenAPI endpoint backend configuration for this Caplet.").optional(),
|
|
18927
|
-
graphqlEndpoint: capletGraphQlEndpointSchema.describe("GraphQL endpoint backend configuration for this Caplet.").optional()
|
|
19001
|
+
graphqlEndpoint: capletGraphQlEndpointSchema.describe("GraphQL endpoint backend configuration for this Caplet.").optional(),
|
|
19002
|
+
httpApi: capletHttpApiSchema.describe("HTTP API backend configuration for this Caplet.").optional()
|
|
18928
19003
|
}).strict().superRefine((frontmatter, ctx) => {
|
|
18929
|
-
if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) !== 1) ctx.addIssue({
|
|
19004
|
+
if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) + Number(Boolean(frontmatter.httpApi)) !== 1) ctx.addIssue({
|
|
18930
19005
|
code: "custom",
|
|
18931
|
-
message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, or
|
|
19006
|
+
message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, graphqlEndpoint, or httpApi"
|
|
18932
19007
|
});
|
|
18933
19008
|
});
|
|
18934
19009
|
function loadCapletFiles(root) {
|
|
@@ -18936,24 +19011,30 @@ function loadCapletFiles(root) {
|
|
|
18936
19011
|
const servers = {};
|
|
18937
19012
|
const openapiEndpoints = {};
|
|
18938
19013
|
const graphqlEndpoints = {};
|
|
19014
|
+
const httpApis = {};
|
|
18939
19015
|
for (const candidate of discoverCapletFiles(root)) {
|
|
18940
|
-
if (servers[candidate.id] || openapiEndpoints[candidate.id] || graphqlEndpoints[candidate.id]) throw new CapletsError("CONFIG_INVALID", `Duplicate Caplet ID ${candidate.id} under ${root}`);
|
|
19016
|
+
if (servers[candidate.id] || openapiEndpoints[candidate.id] || graphqlEndpoints[candidate.id] || httpApis[candidate.id]) throw new CapletsError("CONFIG_INVALID", `Duplicate Caplet ID ${candidate.id} under ${root}`);
|
|
18941
19017
|
const config = readCapletFile(candidate.path);
|
|
18942
|
-
if (isPlainObject$
|
|
19018
|
+
if (isPlainObject$4(config) && config.backend === "openapi") {
|
|
18943
19019
|
const { backend: _backend, ...endpoint } = config;
|
|
18944
19020
|
openapiEndpoints[candidate.id] = endpoint;
|
|
18945
|
-
} else if (isPlainObject$
|
|
19021
|
+
} else if (isPlainObject$4(config) && config.backend === "graphql") {
|
|
18946
19022
|
const { backend: _backend, ...endpoint } = config;
|
|
18947
19023
|
graphqlEndpoints[candidate.id] = endpoint;
|
|
19024
|
+
} else if (isPlainObject$4(config) && config.backend === "http") {
|
|
19025
|
+
const { backend: _backend, ...endpoint } = config;
|
|
19026
|
+
httpApis[candidate.id] = endpoint;
|
|
18948
19027
|
} else servers[candidate.id] = config;
|
|
18949
19028
|
}
|
|
18950
19029
|
const hasServers = Object.keys(servers).length > 0;
|
|
18951
19030
|
const hasOpenApi = Object.keys(openapiEndpoints).length > 0;
|
|
18952
19031
|
const hasGraphQl = Object.keys(graphqlEndpoints).length > 0;
|
|
18953
|
-
|
|
19032
|
+
const hasHttpApis = Object.keys(httpApis).length > 0;
|
|
19033
|
+
return hasServers || hasOpenApi || hasGraphQl || hasHttpApis ? {
|
|
18954
19034
|
...hasServers ? { mcpServers: servers } : {},
|
|
18955
19035
|
...hasOpenApi ? { openapiEndpoints } : {},
|
|
18956
|
-
...hasGraphQl ? { graphqlEndpoints } : {}
|
|
19036
|
+
...hasGraphQl ? { graphqlEndpoints } : {},
|
|
19037
|
+
...hasHttpApis ? { httpApis } : {}
|
|
18957
19038
|
} : void 0;
|
|
18958
19039
|
}
|
|
18959
19040
|
function discoverCapletFiles(root) {
|
|
@@ -19011,6 +19092,14 @@ function capletToServerConfig(frontmatter, body, baseDir) {
|
|
|
19011
19092
|
...frontmatter.tags ? { tags: frontmatter.tags } : {},
|
|
19012
19093
|
body
|
|
19013
19094
|
};
|
|
19095
|
+
if (frontmatter.httpApi) return {
|
|
19096
|
+
...frontmatter.httpApi,
|
|
19097
|
+
backend: "http",
|
|
19098
|
+
name: frontmatter.name,
|
|
19099
|
+
description: frontmatter.description,
|
|
19100
|
+
...frontmatter.tags ? { tags: frontmatter.tags } : {},
|
|
19101
|
+
body
|
|
19102
|
+
};
|
|
19014
19103
|
return {
|
|
19015
19104
|
...frontmatter.mcpServer,
|
|
19016
19105
|
name: frontmatter.name,
|
|
@@ -19053,7 +19142,7 @@ function parseFrontmatter(text, path) {
|
|
|
19053
19142
|
value: text
|
|
19054
19143
|
});
|
|
19055
19144
|
matter(file, { strip: true });
|
|
19056
|
-
if (!isPlainObject$
|
|
19145
|
+
if (!isPlainObject$4(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
|
|
19057
19146
|
return {
|
|
19058
19147
|
frontmatter: file.data.matter,
|
|
19059
19148
|
body: String(file)
|
|
@@ -19062,7 +19151,7 @@ function parseFrontmatter(text, path) {
|
|
|
19062
19151
|
throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid YAML frontmatter`, redactSecrets(error));
|
|
19063
19152
|
}
|
|
19064
19153
|
}
|
|
19065
|
-
function isPlainObject$
|
|
19154
|
+
function isPlainObject$4(value) {
|
|
19066
19155
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19067
19156
|
}
|
|
19068
19157
|
function validateCapletId(id, path) {
|
|
@@ -19071,14 +19160,6 @@ function validateCapletId(id, path) {
|
|
|
19071
19160
|
function hasEnvReference$1(value) {
|
|
19072
19161
|
return /\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$env:[A-Za-z_][A-Za-z0-9_]*/.test(value);
|
|
19073
19162
|
}
|
|
19074
|
-
function isUrl(value) {
|
|
19075
|
-
try {
|
|
19076
|
-
new URL(value);
|
|
19077
|
-
return true;
|
|
19078
|
-
} catch {
|
|
19079
|
-
return false;
|
|
19080
|
-
}
|
|
19081
|
-
}
|
|
19082
19163
|
//#endregion
|
|
19083
19164
|
//#region src/config/paths.ts
|
|
19084
19165
|
const DEFAULT_CONFIG_PATH = join(homedir(), ".caplets", "config.json");
|
|
@@ -19248,7 +19329,45 @@ const publicGraphQlEndpointSchema = object$1({
|
|
|
19248
19329
|
});
|
|
19249
19330
|
});
|
|
19250
19331
|
const normalizedGraphQlEndpointSchema = publicGraphQlEndpointSchema.extend({ body: string().optional() });
|
|
19251
|
-
|
|
19332
|
+
const httpScalarMappingSchema = record(string(), union([
|
|
19333
|
+
string(),
|
|
19334
|
+
number$1(),
|
|
19335
|
+
boolean()
|
|
19336
|
+
]));
|
|
19337
|
+
const httpActionSchema = object$1({
|
|
19338
|
+
method: _enum([
|
|
19339
|
+
"GET",
|
|
19340
|
+
"POST",
|
|
19341
|
+
"PUT",
|
|
19342
|
+
"PATCH",
|
|
19343
|
+
"DELETE"
|
|
19344
|
+
]).describe("HTTP method used for this action."),
|
|
19345
|
+
path: string().min(1).regex(/^\//, "HTTP action path must start with /").describe("URL path appended to the HTTP API baseUrl.").refine((value) => !value.startsWith("//"), "HTTP action path must not start with //").refine((value) => !isUrl(value), "HTTP action path must be a URL path, not a URL"),
|
|
19346
|
+
description: string().min(1).optional().describe("Action capability description."),
|
|
19347
|
+
inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
|
|
19348
|
+
query: httpScalarMappingSchema.optional().describe("Query parameter mapping."),
|
|
19349
|
+
headers: httpScalarMappingSchema.optional().describe("Request header mapping."),
|
|
19350
|
+
jsonBody: unknown().optional().describe("JSON request body mapping.")
|
|
19351
|
+
}).strict().superRefine((action, ctx) => {
|
|
19352
|
+
if (action.method === "GET" && action.jsonBody !== void 0) ctx.addIssue({
|
|
19353
|
+
code: "custom",
|
|
19354
|
+
path: ["jsonBody"],
|
|
19355
|
+
message: "HTTP GET actions must not define jsonBody"
|
|
19356
|
+
});
|
|
19357
|
+
});
|
|
19358
|
+
const publicHttpApiSchema = object$1({
|
|
19359
|
+
name: string().trim().min(1).max(80).describe("Human-readable HTTP API display name."),
|
|
19360
|
+
description: string().describe("Capability description shown to agents before HTTP actions are disclosed.").refine((value) => value.trim().length >= 10, "description must contain at least 10 non-whitespace characters").refine((value) => value.length <= 1500, "description must be at most 1500 characters"),
|
|
19361
|
+
baseUrl: string().url().regex(HTTP_BASE_URL_PATTERN, "HTTP API baseUrl must not include credentials, query, or fragment").describe("Base URL for HTTP action requests."),
|
|
19362
|
+
auth: openApiAuthSchema.describe("Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs."),
|
|
19363
|
+
actions: record(string().regex(SERVER_ID_PATTERN), httpActionSchema).refine((actions) => Object.keys(actions).length > 0, "HTTP API must define at least one action").describe("Configured HTTP actions keyed by stable tool name."),
|
|
19364
|
+
tags: array(string().trim().min(1).max(80)).optional(),
|
|
19365
|
+
requestTimeoutMs: number$1().int().positive().default(6e4).describe("Timeout in milliseconds for HTTP action requests."),
|
|
19366
|
+
maxResponseBytes: number$1().int().positive().default(1e6).describe("Maximum HTTP action response body bytes to read."),
|
|
19367
|
+
disabled: boolean().default(false).describe("When true, omit this HTTP API Caplet.")
|
|
19368
|
+
}).strict();
|
|
19369
|
+
const normalizedHttpApiSchema = publicHttpApiSchema.extend({ body: string().optional() });
|
|
19370
|
+
function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlEndpointValueSchema, httpApiValueSchema) {
|
|
19252
19371
|
return object$1({
|
|
19253
19372
|
$schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
|
|
19254
19373
|
version: literal(1).default(1).describe("Caplets config schema version."),
|
|
@@ -19256,7 +19375,8 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
|
|
|
19256
19375
|
maxSearchLimit: number$1().int().positive().max(50).default(50).describe("Maximum accepted search_tools limit."),
|
|
19257
19376
|
mcpServers: record(string().regex(SERVER_ID_PATTERN), serverValueSchema).default({}).describe("Downstream MCP servers keyed by stable server ID."),
|
|
19258
19377
|
openapiEndpoints: record(string().regex(SERVER_ID_PATTERN), openApiEndpointValueSchema).default({}).describe("OpenAPI endpoints keyed by stable Caplet ID."),
|
|
19259
|
-
graphqlEndpoints: record(string().regex(SERVER_ID_PATTERN), graphQlEndpointValueSchema).default({}).describe("GraphQL endpoints keyed by stable Caplet ID.")
|
|
19378
|
+
graphqlEndpoints: record(string().regex(SERVER_ID_PATTERN), graphQlEndpointValueSchema).default({}).describe("GraphQL endpoints keyed by stable Caplet ID."),
|
|
19379
|
+
httpApis: record(string().regex(SERVER_ID_PATTERN), httpApiValueSchema).default({}).describe("HTTP APIs keyed by stable Caplet ID.")
|
|
19260
19380
|
}).strict().superRefine((config, ctx) => {
|
|
19261
19381
|
if (config.defaultSearchLimit > config.maxSearchLimit) ctx.addIssue({
|
|
19262
19382
|
code: "custom",
|
|
@@ -19410,10 +19530,45 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
|
|
|
19410
19530
|
"auth"
|
|
19411
19531
|
]);
|
|
19412
19532
|
}
|
|
19533
|
+
for (const [endpoint, rawValue] of Object.entries(config.httpApis)) {
|
|
19534
|
+
const raw = rawValue;
|
|
19535
|
+
const duplicateBackend = config.mcpServers[endpoint] ? "mcpServers" : config.openapiEndpoints[endpoint] ? "openapiEndpoints" : config.graphqlEndpoints[endpoint] ? "graphqlEndpoints" : void 0;
|
|
19536
|
+
if (duplicateBackend) ctx.addIssue({
|
|
19537
|
+
code: "custom",
|
|
19538
|
+
path: ["httpApis", endpoint],
|
|
19539
|
+
message: `Caplet ID ${endpoint} is already used by ${duplicateBackend}`
|
|
19540
|
+
});
|
|
19541
|
+
if (!SERVER_ID_PATTERN.test(endpoint)) ctx.addIssue({
|
|
19542
|
+
code: "custom",
|
|
19543
|
+
path: ["httpApis", endpoint],
|
|
19544
|
+
message: "HTTP API ID must match ^[a-zA-Z0-9_-]{1,64}$"
|
|
19545
|
+
});
|
|
19546
|
+
if (raw.baseUrl && !isAllowedHttpBaseUrl(raw.baseUrl)) ctx.addIssue({
|
|
19547
|
+
code: "custom",
|
|
19548
|
+
path: [
|
|
19549
|
+
"httpApis",
|
|
19550
|
+
endpoint,
|
|
19551
|
+
"baseUrl"
|
|
19552
|
+
],
|
|
19553
|
+
message: "HTTP API baseUrl must use https except loopback development urls and must not include credentials, query, or fragment"
|
|
19554
|
+
});
|
|
19555
|
+
validateEndpointAuthHeaders(raw.auth, ctx, [
|
|
19556
|
+
"httpApis",
|
|
19557
|
+
endpoint,
|
|
19558
|
+
"auth"
|
|
19559
|
+
]);
|
|
19560
|
+
for (const [actionName, action] of Object.entries(raw.actions)) if (action.headers) validateHttpActionHeaders(action.headers, ctx, [
|
|
19561
|
+
"httpApis",
|
|
19562
|
+
endpoint,
|
|
19563
|
+
"actions",
|
|
19564
|
+
actionName,
|
|
19565
|
+
"headers"
|
|
19566
|
+
]);
|
|
19567
|
+
}
|
|
19413
19568
|
});
|
|
19414
19569
|
}
|
|
19415
|
-
const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema);
|
|
19416
|
-
const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema);
|
|
19570
|
+
const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema, publicHttpApiSchema);
|
|
19571
|
+
const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema, normalizedHttpApiSchema);
|
|
19417
19572
|
function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConfigPath()) {
|
|
19418
19573
|
const hasUserConfig = existsSync(path);
|
|
19419
19574
|
const hasProjectConfig = existsSync(projectPath);
|
|
@@ -19424,7 +19579,7 @@ function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConf
|
|
|
19424
19579
|
if (!hasUserConfig && !hasProjectConfig && !userCaplets && !projectCaplets) throw new CapletsError("CONFIG_NOT_FOUND", `Caplets config not found at ${path} or ${projectPath}`);
|
|
19425
19580
|
try {
|
|
19426
19581
|
const config = parseConfig(mergeConfigInputs(userConfig, userCaplets, projectConfig, projectCaplets));
|
|
19427
|
-
if (Object.keys(config.mcpServers).length === 0 && Object.keys(config.openapiEndpoints).length === 0 && Object.keys(config.graphqlEndpoints).length === 0) throw new CapletsError("CONFIG_INVALID", "Caplets config must define at least one MCP server, OpenAPI endpoint, or
|
|
19582
|
+
if (Object.keys(config.mcpServers).length === 0 && Object.keys(config.openapiEndpoints).length === 0 && Object.keys(config.graphqlEndpoints).length === 0 && Object.keys(config.httpApis).length === 0) throw new CapletsError("CONFIG_INVALID", "Caplets config must define at least one MCP server, OpenAPI endpoint, GraphQL endpoint, or HTTP API");
|
|
19428
19583
|
return config;
|
|
19429
19584
|
} catch (error) {
|
|
19430
19585
|
if (error instanceof CapletsError) throw error;
|
|
@@ -19454,7 +19609,7 @@ function normalizeLocalPaths(input, baseDir) {
|
|
|
19454
19609
|
}
|
|
19455
19610
|
function normalizeEndpointPaths(endpoints, baseDir, normalize) {
|
|
19456
19611
|
if (!endpoints) return;
|
|
19457
|
-
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$
|
|
19612
|
+
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$3(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
|
|
19458
19613
|
}
|
|
19459
19614
|
function normalizeOpenApiPath(endpoint, baseDir) {
|
|
19460
19615
|
return {
|
|
@@ -19463,7 +19618,7 @@ function normalizeOpenApiPath(endpoint, baseDir) {
|
|
|
19463
19618
|
};
|
|
19464
19619
|
}
|
|
19465
19620
|
function normalizeGraphQlPath(endpoint, baseDir) {
|
|
19466
|
-
const operations = isPlainObject$
|
|
19621
|
+
const operations = isPlainObject$3(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$3(operation) ? {
|
|
19467
19622
|
...operation,
|
|
19468
19623
|
documentPath: normalizeLocalPath(operation.documentPath, baseDir)
|
|
19469
19624
|
} : operation])) : endpoint.operations;
|
|
@@ -19480,6 +19635,7 @@ function normalizeLocalPath(value, baseDir) {
|
|
|
19480
19635
|
function rejectUntrustedProjectExecutableBackends(input, path) {
|
|
19481
19636
|
if (input.openapiEndpoints && Object.keys(input.openapiEndpoints).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define openapiEndpoints; use trusted project Caplet files or user config`);
|
|
19482
19637
|
if (input.graphqlEndpoints && Object.keys(input.graphqlEndpoints).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define graphqlEndpoints; use trusted project Caplet files or user config`);
|
|
19638
|
+
if (input.httpApis && Object.keys(input.httpApis).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define httpApis; use trusted project Caplet files or user config`);
|
|
19483
19639
|
return input;
|
|
19484
19640
|
}
|
|
19485
19641
|
function mergeConfigInputs(...inputs) {
|
|
@@ -19500,6 +19656,10 @@ function mergeConfigInputs(...inputs) {
|
|
|
19500
19656
|
graphqlEndpoints: {
|
|
19501
19657
|
...merged?.graphqlEndpoints,
|
|
19502
19658
|
...input.graphqlEndpoints
|
|
19659
|
+
},
|
|
19660
|
+
httpApis: {
|
|
19661
|
+
...merged?.httpApis,
|
|
19662
|
+
...input.httpApis
|
|
19503
19663
|
}
|
|
19504
19664
|
};
|
|
19505
19665
|
}
|
|
@@ -19530,6 +19690,12 @@ function parseConfig(input) {
|
|
|
19530
19690
|
server,
|
|
19531
19691
|
backend: "graphql"
|
|
19532
19692
|
});
|
|
19693
|
+
const httpApis = {};
|
|
19694
|
+
for (const [server, raw] of Object.entries(parsed.data.httpApis)) httpApis[server] = stripUndefined({
|
|
19695
|
+
...raw,
|
|
19696
|
+
server,
|
|
19697
|
+
backend: "http"
|
|
19698
|
+
});
|
|
19533
19699
|
return {
|
|
19534
19700
|
version: parsed.data.version,
|
|
19535
19701
|
options: {
|
|
@@ -19538,7 +19704,8 @@ function parseConfig(input) {
|
|
|
19538
19704
|
},
|
|
19539
19705
|
mcpServers: servers,
|
|
19540
19706
|
openapiEndpoints,
|
|
19541
|
-
graphqlEndpoints
|
|
19707
|
+
graphqlEndpoints,
|
|
19708
|
+
httpApis
|
|
19542
19709
|
};
|
|
19543
19710
|
}
|
|
19544
19711
|
function validateEndpointAuthHeaders(auth, ctx, path) {
|
|
@@ -19567,10 +19734,10 @@ function interpolateConfig(value, path = []) {
|
|
|
19567
19734
|
return value;
|
|
19568
19735
|
}
|
|
19569
19736
|
function isPublicMetadataPath(path) {
|
|
19570
|
-
if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints") return false;
|
|
19737
|
+
if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis") return false;
|
|
19571
19738
|
return NON_INTERPOLATED_SERVER_FIELDS.has(path[2] ?? "");
|
|
19572
19739
|
}
|
|
19573
|
-
function isPlainObject$
|
|
19740
|
+
function isPlainObject$3(value) {
|
|
19574
19741
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19575
19742
|
}
|
|
19576
19743
|
function hasEnvReference(value) {
|
|
@@ -19629,7 +19796,8 @@ function authTargets(config) {
|
|
|
19629
19796
|
return [
|
|
19630
19797
|
...Object.values(config.mcpServers).filter((server) => server.transport !== "stdio" && (server.auth?.type === "oauth2" || server.auth?.type === "oidc")),
|
|
19631
19798
|
...Object.values(config.openapiEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc"),
|
|
19632
|
-
...Object.values(config.graphqlEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc").map(graphQlAuthTarget)
|
|
19799
|
+
...Object.values(config.graphqlEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc").map(graphQlAuthTarget),
|
|
19800
|
+
...Object.values(config.httpApis).filter((api) => api.auth?.type === "oauth2" || api.auth?.type === "oidc").map(httpAuthTarget)
|
|
19633
19801
|
];
|
|
19634
19802
|
}
|
|
19635
19803
|
function graphQlAuthTarget(endpoint) {
|
|
@@ -19638,6 +19806,9 @@ function graphQlAuthTarget(endpoint) {
|
|
|
19638
19806
|
url: endpoint.endpointUrl
|
|
19639
19807
|
};
|
|
19640
19808
|
}
|
|
19809
|
+
function httpAuthTarget(api) {
|
|
19810
|
+
return { ...api };
|
|
19811
|
+
}
|
|
19641
19812
|
function assertLoginTarget(target, serverId) {
|
|
19642
19813
|
if (!target) throw new CapletsError("SERVER_NOT_FOUND", `Server ${serverId} is not configured for OAuth`);
|
|
19643
19814
|
if ("disabled" in target && target.disabled) throw new CapletsError("SERVER_UNAVAILABLE", `Server ${serverId} is disabled`);
|
|
@@ -25649,7 +25820,7 @@ var Protocol = class {
|
|
|
25649
25820
|
};
|
|
25650
25821
|
}
|
|
25651
25822
|
};
|
|
25652
|
-
function isPlainObject$
|
|
25823
|
+
function isPlainObject$2(value) {
|
|
25653
25824
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25654
25825
|
}
|
|
25655
25826
|
function mergeCapabilities(base, additional) {
|
|
@@ -25659,7 +25830,7 @@ function mergeCapabilities(base, additional) {
|
|
|
25659
25830
|
const addValue = additional[k];
|
|
25660
25831
|
if (addValue === void 0) continue;
|
|
25661
25832
|
const baseValue = result[k];
|
|
25662
|
-
if (isPlainObject$
|
|
25833
|
+
if (isPlainObject$2(baseValue) && isPlainObject$2(addValue)) result[k] = {
|
|
25663
25834
|
...baseValue,
|
|
25664
25835
|
...addValue
|
|
25665
25836
|
};
|
|
@@ -50843,6 +51014,279 @@ function graphQlCacheKey(endpoint) {
|
|
|
50843
51014
|
});
|
|
50844
51015
|
}
|
|
50845
51016
|
//#endregion
|
|
51017
|
+
//#region src/http-actions.ts
|
|
51018
|
+
const DEFAULT_INPUT_SCHEMA = {
|
|
51019
|
+
type: "object",
|
|
51020
|
+
additionalProperties: true
|
|
51021
|
+
};
|
|
51022
|
+
var HttpActionManager = class {
|
|
51023
|
+
registry;
|
|
51024
|
+
options;
|
|
51025
|
+
constructor(registry, options = {}) {
|
|
51026
|
+
this.registry = registry;
|
|
51027
|
+
this.options = options;
|
|
51028
|
+
}
|
|
51029
|
+
updateRegistry(registry) {
|
|
51030
|
+
this.registry = registry;
|
|
51031
|
+
}
|
|
51032
|
+
invalidate(_serverId) {}
|
|
51033
|
+
async checkApi(api) {
|
|
51034
|
+
const startedAt = Date.now();
|
|
51035
|
+
try {
|
|
51036
|
+
const operations = operationsFor(api);
|
|
51037
|
+
validateBaseUrl(api);
|
|
51038
|
+
authHeaders$1(api, this.options.authDir);
|
|
51039
|
+
for (const operation of operations) validateAction(api, operation);
|
|
51040
|
+
this.registry.setStatus(api.server, "available");
|
|
51041
|
+
return {
|
|
51042
|
+
server: api.server,
|
|
51043
|
+
status: "available",
|
|
51044
|
+
toolCount: operations.length,
|
|
51045
|
+
elapsedMs: Date.now() - startedAt
|
|
51046
|
+
};
|
|
51047
|
+
} catch (error) {
|
|
51048
|
+
const safe = toSafeError(error, "SERVER_UNAVAILABLE");
|
|
51049
|
+
this.registry.setStatus(api.server, "unavailable", safe);
|
|
51050
|
+
return {
|
|
51051
|
+
server: api.server,
|
|
51052
|
+
status: "unavailable",
|
|
51053
|
+
elapsedMs: Date.now() - startedAt,
|
|
51054
|
+
error: safe
|
|
51055
|
+
};
|
|
51056
|
+
}
|
|
51057
|
+
}
|
|
51058
|
+
async listTools(api) {
|
|
51059
|
+
return operationsFor(api).map((operation) => this.toTool(operation));
|
|
51060
|
+
}
|
|
51061
|
+
async getTool(api, toolName) {
|
|
51062
|
+
return this.toTool(getOperation(api, toolName));
|
|
51063
|
+
}
|
|
51064
|
+
async callTool(api, toolName, args) {
|
|
51065
|
+
const operation = getOperation(api, toolName);
|
|
51066
|
+
const startedAt = Date.now();
|
|
51067
|
+
const request = buildRequest$1(api, operation, args, this.options.authDir);
|
|
51068
|
+
const controller = new AbortController();
|
|
51069
|
+
const timeout = setTimeout(() => controller.abort(), api.requestTimeoutMs);
|
|
51070
|
+
try {
|
|
51071
|
+
const response = await fetch(request.url, {
|
|
51072
|
+
method: operation.method,
|
|
51073
|
+
headers: request.headers,
|
|
51074
|
+
redirect: "manual",
|
|
51075
|
+
signal: controller.signal,
|
|
51076
|
+
...request.body === void 0 ? {} : { body: request.body }
|
|
51077
|
+
});
|
|
51078
|
+
if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "HTTP action request returned a redirect", {
|
|
51079
|
+
server: api.server,
|
|
51080
|
+
status: response.status,
|
|
51081
|
+
location: response.headers.get("location") ? "[REDACTED]" : void 0
|
|
51082
|
+
});
|
|
51083
|
+
const parsed = await readResponse$1(response, api, Date.now() - startedAt);
|
|
51084
|
+
return {
|
|
51085
|
+
content: [{
|
|
51086
|
+
type: "text",
|
|
51087
|
+
text: JSON.stringify(parsed, null, 2)
|
|
51088
|
+
}],
|
|
51089
|
+
structuredContent: parsed,
|
|
51090
|
+
isError: !response.ok
|
|
51091
|
+
};
|
|
51092
|
+
} catch (error) {
|
|
51093
|
+
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `HTTP action request timed out for ${api.server}/${toolName}`);
|
|
51094
|
+
if (error instanceof CapletsError) throw error;
|
|
51095
|
+
throw new CapletsError("DOWNSTREAM_TOOL_ERROR", `HTTP action request failed for ${api.server}/${toolName}`, toSafeError(error));
|
|
51096
|
+
} finally {
|
|
51097
|
+
clearTimeout(timeout);
|
|
51098
|
+
}
|
|
51099
|
+
}
|
|
51100
|
+
compact(api, tool) {
|
|
51101
|
+
return {
|
|
51102
|
+
server: api.server,
|
|
51103
|
+
tool: tool.name,
|
|
51104
|
+
...tool.description ? { description: tool.description } : {},
|
|
51105
|
+
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
51106
|
+
hasInputSchema: Boolean(tool.inputSchema)
|
|
51107
|
+
};
|
|
51108
|
+
}
|
|
51109
|
+
search(api, tools, query, limit) {
|
|
51110
|
+
const needle = query.toLocaleLowerCase();
|
|
51111
|
+
return tools.filter((tool) => `${tool.name}\n${tool.description ?? ""}`.toLocaleLowerCase().includes(needle)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, limit).map((tool) => this.compact(api, tool));
|
|
51112
|
+
}
|
|
51113
|
+
toTool(operation) {
|
|
51114
|
+
return {
|
|
51115
|
+
name: operation.name,
|
|
51116
|
+
...operation.description ? { description: operation.description } : {},
|
|
51117
|
+
inputSchema: operation.inputSchema ?? DEFAULT_INPUT_SCHEMA,
|
|
51118
|
+
annotations: {
|
|
51119
|
+
readOnlyHint: operation.method === "GET",
|
|
51120
|
+
destructiveHint: operation.method === "DELETE"
|
|
51121
|
+
}
|
|
51122
|
+
};
|
|
51123
|
+
}
|
|
51124
|
+
};
|
|
51125
|
+
function operationsFor(api) {
|
|
51126
|
+
return Object.entries(api.actions).map(([name, action]) => ({
|
|
51127
|
+
name,
|
|
51128
|
+
...action
|
|
51129
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
51130
|
+
}
|
|
51131
|
+
function getOperation(api, toolName) {
|
|
51132
|
+
const operations = operationsFor(api);
|
|
51133
|
+
const operation = operations.find((candidate) => candidate.name === toolName);
|
|
51134
|
+
if (!operation) throw new CapletsError("TOOL_NOT_FOUND", `Tool ${toolName} was not found on ${api.server}`, {
|
|
51135
|
+
server: api.server,
|
|
51136
|
+
tool: toolName,
|
|
51137
|
+
suggestions: operations.map((candidate) => candidate.name).filter((name) => name.toLocaleLowerCase().includes(toolName.toLocaleLowerCase()[0] ?? "")).slice(0, 5)
|
|
51138
|
+
});
|
|
51139
|
+
return operation;
|
|
51140
|
+
}
|
|
51141
|
+
function buildRequest$1(api, operation, args, authDir) {
|
|
51142
|
+
validateBaseUrl(api);
|
|
51143
|
+
validateAction(api, operation);
|
|
51144
|
+
const url = buildActionUrl(api.baseUrl, substitutePath$1(operation.path, args, operation), { allowEncodedSlash: true });
|
|
51145
|
+
const query = resolveMappingToRecord(operation.query, args, "query");
|
|
51146
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null) url.searchParams.append(key, serializeHttpValue$1("query", key, value));
|
|
51147
|
+
const headers = new Headers();
|
|
51148
|
+
applyAuth$1(headers, api, authDir);
|
|
51149
|
+
const resolvedHeaders = resolveMappingToRecord(operation.headers, args, "headers");
|
|
51150
|
+
for (const [key, value] of Object.entries(resolvedHeaders)) if (value !== void 0 && value !== null) {
|
|
51151
|
+
validateResolvedHeader(api, operation, key);
|
|
51152
|
+
headers.set(key, serializeHttpValue$1("header", key, value));
|
|
51153
|
+
}
|
|
51154
|
+
const bodyValue = resolveMapping(operation.jsonBody, args);
|
|
51155
|
+
if (operation.jsonBody !== void 0) {
|
|
51156
|
+
if (bodyValue === void 0) throw new CapletsError("REQUEST_INVALID", "HTTP action jsonBody must not resolve to undefined");
|
|
51157
|
+
headers.set("content-type", "application/json");
|
|
51158
|
+
return {
|
|
51159
|
+
url,
|
|
51160
|
+
headers,
|
|
51161
|
+
body: JSON.stringify(bodyValue)
|
|
51162
|
+
};
|
|
51163
|
+
}
|
|
51164
|
+
return {
|
|
51165
|
+
url,
|
|
51166
|
+
headers
|
|
51167
|
+
};
|
|
51168
|
+
}
|
|
51169
|
+
function substitutePath$1(path, args, operation) {
|
|
51170
|
+
return path.replace(/\{([^}]+)\}/g, (_match, name) => {
|
|
51171
|
+
const value = args[name];
|
|
51172
|
+
if (value === void 0 || value === null || value === "") throw new CapletsError("REQUEST_INVALID", `Missing required path parameter ${name}`, { tool: operation.name });
|
|
51173
|
+
return encodeURIComponent(serializeHttpValue$1("path", name, value));
|
|
51174
|
+
});
|
|
51175
|
+
}
|
|
51176
|
+
function resolveMapping(mapping, input) {
|
|
51177
|
+
if (typeof mapping === "string") {
|
|
51178
|
+
if (mapping === "$input") return input;
|
|
51179
|
+
if (mapping.startsWith("$input.")) return valueAtPath(input, mapping.slice(7));
|
|
51180
|
+
return mapping;
|
|
51181
|
+
}
|
|
51182
|
+
if (Array.isArray(mapping)) return mapping.map((item) => resolveMapping(item, input));
|
|
51183
|
+
if (mapping && typeof mapping === "object") return Object.fromEntries(Object.entries(mapping).map(([key, value]) => [key, resolveMapping(value, input)]));
|
|
51184
|
+
return mapping;
|
|
51185
|
+
}
|
|
51186
|
+
function resolveMappingToRecord(mapping, input, name) {
|
|
51187
|
+
if (mapping === void 0) return {};
|
|
51188
|
+
const resolved = resolveMapping(mapping, input);
|
|
51189
|
+
if (!isPlainObject$1(resolved)) throw new CapletsError("REQUEST_INVALID", `HTTP action ${name} mapping must resolve to an object`);
|
|
51190
|
+
return resolved;
|
|
51191
|
+
}
|
|
51192
|
+
function valueAtPath(input, path) {
|
|
51193
|
+
let current = input;
|
|
51194
|
+
for (const segment of path.split(".")) {
|
|
51195
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
51196
|
+
current = current[segment];
|
|
51197
|
+
}
|
|
51198
|
+
return current;
|
|
51199
|
+
}
|
|
51200
|
+
function validateAction(api, operation) {
|
|
51201
|
+
buildActionUrl(api.baseUrl, operation.path);
|
|
51202
|
+
validateConfiguredHeaders(api, operation);
|
|
51203
|
+
}
|
|
51204
|
+
function validateConfiguredHeaders(api, operation) {
|
|
51205
|
+
const configured = asRecord$1(operation.headers);
|
|
51206
|
+
for (const key of Object.keys(configured)) validateResolvedHeader(api, operation, key);
|
|
51207
|
+
}
|
|
51208
|
+
function validateResolvedHeader(api, operation, key) {
|
|
51209
|
+
const authHeaderNames = api.auth.type === "headers" ? new Set(Object.keys(api.auth.headers).map((header) => header.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
51210
|
+
const normalized = key.toLowerCase();
|
|
51211
|
+
if (FORBIDDEN_HEADERS.has(normalized) || authHeaderNames.has(normalized)) throw new CapletsError("CONFIG_INVALID", `HTTP action header ${key} is not allowed`, {
|
|
51212
|
+
server: api.server,
|
|
51213
|
+
tool: operation.name
|
|
51214
|
+
});
|
|
51215
|
+
}
|
|
51216
|
+
function serializeHttpValue$1(location, name, value) {
|
|
51217
|
+
switch (typeof value) {
|
|
51218
|
+
case "string":
|
|
51219
|
+
case "number":
|
|
51220
|
+
case "boolean": return String(value);
|
|
51221
|
+
default: throw new CapletsError("REQUEST_INVALID", `HTTP action ${location} parameter ${name} must be a string, number, or boolean`);
|
|
51222
|
+
}
|
|
51223
|
+
}
|
|
51224
|
+
function applyAuth$1(headers, api, authDir) {
|
|
51225
|
+
for (const [key, value] of Object.entries(authHeaders$1(api, authDir))) headers.set(key, value);
|
|
51226
|
+
}
|
|
51227
|
+
function authHeaders$1(api, authDir) {
|
|
51228
|
+
switch (api.auth.type) {
|
|
51229
|
+
case "none": return {};
|
|
51230
|
+
case "bearer": return { authorization: `Bearer ${api.auth.token}` };
|
|
51231
|
+
case "headers": return api.auth.headers;
|
|
51232
|
+
case "oauth2":
|
|
51233
|
+
case "oidc": return genericOAuthHeaders({
|
|
51234
|
+
server: api.server,
|
|
51235
|
+
backend: "http",
|
|
51236
|
+
baseUrl: api.baseUrl,
|
|
51237
|
+
auth: api.auth,
|
|
51238
|
+
requestTimeoutMs: api.requestTimeoutMs
|
|
51239
|
+
}, authDir);
|
|
51240
|
+
}
|
|
51241
|
+
}
|
|
51242
|
+
async function readResponse$1(response, api, elapsedMs) {
|
|
51243
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
51244
|
+
const body = parseHttpBody(contentType, await readLimitedText(response, {
|
|
51245
|
+
maxBytes: maxResponseBytes(api),
|
|
51246
|
+
errorMessage: "HTTP action response exceeded byte limit"
|
|
51247
|
+
}));
|
|
51248
|
+
return {
|
|
51249
|
+
status: response.status,
|
|
51250
|
+
statusText: response.statusText,
|
|
51251
|
+
headers: { "content-type": contentType },
|
|
51252
|
+
...body === void 0 ? {} : { body },
|
|
51253
|
+
elapsedMs
|
|
51254
|
+
};
|
|
51255
|
+
}
|
|
51256
|
+
function maxResponseBytes(api) {
|
|
51257
|
+
return api.maxResponseBytes;
|
|
51258
|
+
}
|
|
51259
|
+
function validateBaseUrl(api) {
|
|
51260
|
+
if (!isAllowedRemoteUrl(api.baseUrl)) throw new CapletsError("CONFIG_INVALID", `${api.server} HTTP API baseUrl is not allowed`);
|
|
51261
|
+
const url = new URL(api.baseUrl);
|
|
51262
|
+
if (url.username || url.password || url.search || url.hash) throw new CapletsError("CONFIG_INVALID", `${api.server} HTTP API baseUrl must not include credentials, query, or fragment`);
|
|
51263
|
+
}
|
|
51264
|
+
function buildActionUrl(base, actionPath, options = {}) {
|
|
51265
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(actionPath) || actionPath.startsWith("//")) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot change origin");
|
|
51266
|
+
for (const rawSegment of actionPath.split("/")) {
|
|
51267
|
+
let segment;
|
|
51268
|
+
try {
|
|
51269
|
+
segment = decodeURIComponent(rawSegment);
|
|
51270
|
+
} catch {
|
|
51271
|
+
throw new CapletsError("CONFIG_INVALID", "HTTP action path contains invalid encoding");
|
|
51272
|
+
}
|
|
51273
|
+
if (segment === "." || segment === ".." || !options.allowEncodedSlash && segment.includes("/")) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot contain dot segments");
|
|
51274
|
+
}
|
|
51275
|
+
const baseUrl = new URL(base);
|
|
51276
|
+
const originalOrigin = baseUrl.origin;
|
|
51277
|
+
const basePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
51278
|
+
baseUrl.pathname = [basePath, actionPath.replace(/^\/+/, "")].filter(Boolean).join("/");
|
|
51279
|
+
if (baseUrl.origin !== originalOrigin) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot change origin");
|
|
51280
|
+
if (basePath && baseUrl.pathname !== basePath && !baseUrl.pathname.startsWith(`${basePath}/`)) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot escape baseUrl path");
|
|
51281
|
+
return baseUrl;
|
|
51282
|
+
}
|
|
51283
|
+
function asRecord$1(value) {
|
|
51284
|
+
return isPlainObject$1(value) ? value : {};
|
|
51285
|
+
}
|
|
51286
|
+
function isPlainObject$1(value) {
|
|
51287
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
51288
|
+
}
|
|
51289
|
+
//#endregion
|
|
50846
51290
|
//#region node_modules/.pnpm/@apidevtools+swagger-parser@12.1.0_openapi-types@12.1.3/node_modules/@apidevtools/swagger-parser/lib/util.js
|
|
50847
51291
|
var require_util = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
50848
51292
|
const util = __require("util");
|
|
@@ -60731,7 +61175,7 @@ var ServerRegistry = class {
|
|
|
60731
61175
|
return this.allCaplets().filter((server) => !server.disabled);
|
|
60732
61176
|
}
|
|
60733
61177
|
get(serverId) {
|
|
60734
|
-
const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId];
|
|
61178
|
+
const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId] ?? this.config.httpApis[serverId];
|
|
60735
61179
|
return server?.disabled ? void 0 : server;
|
|
60736
61180
|
}
|
|
60737
61181
|
require(serverId) {
|
|
@@ -60781,12 +61225,13 @@ var ServerRegistry = class {
|
|
|
60781
61225
|
return [
|
|
60782
61226
|
...Object.values(this.config.mcpServers),
|
|
60783
61227
|
...Object.values(this.config.openapiEndpoints),
|
|
60784
|
-
...Object.values(this.config.graphqlEndpoints)
|
|
61228
|
+
...Object.values(this.config.graphqlEndpoints),
|
|
61229
|
+
...Object.values(this.config.httpApis)
|
|
60785
61230
|
];
|
|
60786
61231
|
}
|
|
60787
61232
|
};
|
|
60788
61233
|
function capabilityDescription(server) {
|
|
60789
|
-
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : "GraphQL endpoint";
|
|
61234
|
+
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
|
|
60790
61235
|
const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
|
|
60791
61236
|
const hint = [
|
|
60792
61237
|
`Use this Caplet to inspect and call tools from its ${backendName} backend.`,
|
|
@@ -60818,6 +61263,12 @@ function backendDetail(server) {
|
|
|
60818
61263
|
source: graphQlSource(server),
|
|
60819
61264
|
configuredOperations: Boolean(server.operations && Object.keys(server.operations).length > 0)
|
|
60820
61265
|
};
|
|
61266
|
+
if (server.backend === "http") return {
|
|
61267
|
+
type: "http",
|
|
61268
|
+
disabled: server.disabled,
|
|
61269
|
+
requestTimeoutMs: server.requestTimeoutMs,
|
|
61270
|
+
configuredActions: Object.keys(server.actions).length
|
|
61271
|
+
};
|
|
60821
61272
|
return {
|
|
60822
61273
|
type: "mcp",
|
|
60823
61274
|
transport: server.transport,
|
|
@@ -60854,16 +61305,16 @@ const generatedToolInputSchema = object$1({
|
|
|
60854
61305
|
tool: string().optional().describe("Exact downstream tool name for get_tool or call_tool. Example: {\"operation\":\"get_tool\",\"tool\":\"web_search_exa\"} before calling it."),
|
|
60855
61306
|
arguments: record(string(), unknown()).optional().describe("Required JSON object only for call_tool. Put every downstream tool input inside this object. Example: {\"operation\":\"call_tool\",\"tool\":\"web_search_exa\",\"arguments\":{\"query\":\"latest MCP docs\",\"numResults\":3}}. Do not send downstream inputs as top-level query, limit, url, path, or other fields.")
|
|
60856
61307
|
}).strict();
|
|
60857
|
-
async function handleServerTool(server, request, registry, downstream, openapi, graphql) {
|
|
61308
|
+
async function handleServerTool(server, request, registry, downstream, openapi, graphql, http) {
|
|
60858
61309
|
const parsed = validateOperationRequest(request, registry.config.options.maxSearchLimit);
|
|
60859
61310
|
switch (parsed.operation) {
|
|
60860
61311
|
case "get_caplet": return jsonResult(registry.detail(server));
|
|
60861
|
-
case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql).check(server));
|
|
61312
|
+
case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql, http).check(server));
|
|
60862
61313
|
case "check_mcp_server":
|
|
60863
61314
|
if (server.backend !== "mcp") throw new CapletsError("REQUEST_INVALID", "check_mcp_server is only valid for MCP-backed Caplets; use check_backend");
|
|
60864
61315
|
return jsonResult(await downstream.checkServer(server));
|
|
60865
61316
|
case "list_tools": {
|
|
60866
|
-
const backend = backendFor(server, downstream, openapi, graphql);
|
|
61317
|
+
const backend = backendFor(server, downstream, openapi, graphql, http);
|
|
60867
61318
|
const tools = await backend.listTools(server);
|
|
60868
61319
|
return jsonResult({
|
|
60869
61320
|
server: server.server,
|
|
@@ -60871,7 +61322,7 @@ async function handleServerTool(server, request, registry, downstream, openapi,
|
|
|
60871
61322
|
});
|
|
60872
61323
|
}
|
|
60873
61324
|
case "search_tools": {
|
|
60874
|
-
const backend = backendFor(server, downstream, openapi, graphql);
|
|
61325
|
+
const backend = backendFor(server, downstream, openapi, graphql, http);
|
|
60875
61326
|
const tools = await backend.listTools(server);
|
|
60876
61327
|
const limit = parsed.limit ?? registry.config.options.defaultSearchLimit;
|
|
60877
61328
|
return jsonResult({
|
|
@@ -60881,13 +61332,13 @@ async function handleServerTool(server, request, registry, downstream, openapi,
|
|
|
60881
61332
|
});
|
|
60882
61333
|
}
|
|
60883
61334
|
case "get_tool": {
|
|
60884
|
-
const tool = await backendFor(server, downstream, openapi, graphql).getTool(server, parsed.tool);
|
|
61335
|
+
const tool = await backendFor(server, downstream, openapi, graphql, http).getTool(server, parsed.tool);
|
|
60885
61336
|
return jsonResult({
|
|
60886
61337
|
server: server.server,
|
|
60887
61338
|
tool
|
|
60888
61339
|
});
|
|
60889
61340
|
}
|
|
60890
|
-
case "call_tool": return backendFor(server, downstream, openapi, graphql).callTool(server, parsed.tool, parsed.arguments);
|
|
61341
|
+
case "call_tool": return backendFor(server, downstream, openapi, graphql, http).callTool(server, parsed.tool, parsed.arguments);
|
|
60891
61342
|
}
|
|
60892
61343
|
}
|
|
60893
61344
|
function validateOperationRequest(request, maxSearchLimit) {
|
|
@@ -60950,7 +61401,7 @@ function jsonResult(value) {
|
|
|
60950
61401
|
function isPlainObject(value) {
|
|
60951
61402
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60952
61403
|
}
|
|
60953
|
-
function backendFor(server, downstream, openapi, graphql) {
|
|
61404
|
+
function backendFor(server, downstream, openapi, graphql, http) {
|
|
60954
61405
|
if (server.backend === "mcp") return {
|
|
60955
61406
|
check: (...args) => downstream.checkServer(...args),
|
|
60956
61407
|
listTools: (...args) => downstream.listTools(...args),
|
|
@@ -60970,6 +61421,17 @@ function backendFor(server, downstream, openapi, graphql) {
|
|
|
60970
61421
|
search: (...args) => graphql.search(...args)
|
|
60971
61422
|
};
|
|
60972
61423
|
}
|
|
61424
|
+
if (server.backend === "http") {
|
|
61425
|
+
if (!http) throw new CapletsError("INTERNAL_ERROR", "HTTP action manager is not configured");
|
|
61426
|
+
return {
|
|
61427
|
+
check: (...args) => http.checkApi(...args),
|
|
61428
|
+
listTools: (...args) => http.listTools(...args),
|
|
61429
|
+
getTool: (...args) => http.getTool(...args),
|
|
61430
|
+
callTool: (...args) => http.callTool(...args),
|
|
61431
|
+
compact: (...args) => http.compact(...args),
|
|
61432
|
+
search: (...args) => http.search(...args)
|
|
61433
|
+
};
|
|
61434
|
+
}
|
|
60973
61435
|
if (!openapi) throw new CapletsError("INTERNAL_ERROR", "OpenAPI manager is not configured");
|
|
60974
61436
|
return {
|
|
60975
61437
|
check: (...args) => openapi.checkEndpoint(...args),
|
|
@@ -60988,6 +61450,7 @@ var CapletsRuntime = class {
|
|
|
60988
61450
|
downstream;
|
|
60989
61451
|
openapi;
|
|
60990
61452
|
graphql;
|
|
61453
|
+
http;
|
|
60991
61454
|
tools = /* @__PURE__ */ new Map();
|
|
60992
61455
|
paths;
|
|
60993
61456
|
watchDebounceMs;
|
|
@@ -61008,6 +61471,7 @@ var CapletsRuntime = class {
|
|
|
61008
61471
|
this.downstream = new DownstreamManager(this.registry, selectAuthOptions(options.authDir));
|
|
61009
61472
|
this.openapi = new OpenApiManager(this.registry, selectAuthOptions(options.authDir));
|
|
61010
61473
|
this.graphql = new GraphQLManager(this.registry, selectAuthOptions(options.authDir));
|
|
61474
|
+
this.http = new HttpActionManager(this.registry, selectAuthOptions(options.authDir));
|
|
61011
61475
|
this.server = options.server ?? new McpServer({
|
|
61012
61476
|
name: "caplets",
|
|
61013
61477
|
version
|
|
@@ -61083,6 +61547,7 @@ var CapletsRuntime = class {
|
|
|
61083
61547
|
this.downstream.updateRegistry(nextRegistry);
|
|
61084
61548
|
this.openapi.updateRegistry(nextRegistry);
|
|
61085
61549
|
this.graphql.updateRegistry(nextRegistry);
|
|
61550
|
+
this.http.updateRegistry(nextRegistry);
|
|
61086
61551
|
let invalidated = true;
|
|
61087
61552
|
try {
|
|
61088
61553
|
await this.invalidateChangedBackends(previousConfig, nextConfig);
|
|
@@ -61141,7 +61606,7 @@ var CapletsRuntime = class {
|
|
|
61141
61606
|
}
|
|
61142
61607
|
async handleTool(serverId, request) {
|
|
61143
61608
|
try {
|
|
61144
|
-
return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql);
|
|
61609
|
+
return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql, this.http);
|
|
61145
61610
|
} catch (error) {
|
|
61146
61611
|
return errorResult(error);
|
|
61147
61612
|
}
|
|
@@ -61157,6 +61622,7 @@ var CapletsRuntime = class {
|
|
|
61157
61622
|
if (before?.backend === "mcp") await this.downstream.closeServer(serverId);
|
|
61158
61623
|
if (before?.backend === "openapi" || after?.backend === "openapi" || !after) this.openapi.invalidate(serverId);
|
|
61159
61624
|
if (before?.backend === "graphql" || after?.backend === "graphql" || !after) this.graphql.invalidate(serverId);
|
|
61625
|
+
if (before?.backend === "http" || after?.backend === "http" || !after) this.http.invalidate(serverId);
|
|
61160
61626
|
}
|
|
61161
61627
|
}
|
|
61162
61628
|
resetWatchers() {
|
|
@@ -61248,14 +61714,15 @@ function allCaplets(config) {
|
|
|
61248
61714
|
return [
|
|
61249
61715
|
...Object.values(config.mcpServers),
|
|
61250
61716
|
...Object.values(config.openapiEndpoints),
|
|
61251
|
-
...Object.values(config.graphqlEndpoints)
|
|
61717
|
+
...Object.values(config.graphqlEndpoints),
|
|
61718
|
+
...Object.values(config.httpApis)
|
|
61252
61719
|
];
|
|
61253
61720
|
}
|
|
61254
61721
|
function nextEnabledServers(config) {
|
|
61255
61722
|
return allCaplets(config).filter((server) => !server.disabled);
|
|
61256
61723
|
}
|
|
61257
61724
|
function capletById(config, serverId) {
|
|
61258
|
-
return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId];
|
|
61725
|
+
return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId] ?? config.httpApis[serverId];
|
|
61259
61726
|
}
|
|
61260
61727
|
function serializeCaplet(caplet) {
|
|
61261
61728
|
return JSON.stringify(caplet ?? null);
|