caplets 0.7.0 → 0.9.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 +120 -19
- package/dist/index.js +589 -78
- package/package.json +1 -1
- package/schemas/caplet.schema.json +306 -0
- package/schemas/caplets-config.schema.json +334 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import minproc, { stdin, stdout, default as process$1 } from "node:process";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
import minpath, { basename, dirname, extname, isAbsolute, join, parse, relative, resolve,
|
|
5
|
+
import minpath, { basename, dirname, extname, isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:path";
|
|
6
6
|
import { accessSync, chmodSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { createServer } from "node:http";
|
|
@@ -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.9.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;
|
|
@@ -10658,6 +10658,41 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
|
|
|
10658
10658
|
return OAuthClientInformationFullSchema.parse(await response.json());
|
|
10659
10659
|
}
|
|
10660
10660
|
//#endregion
|
|
10661
|
+
//#region src/config/paths.ts
|
|
10662
|
+
function defaultConfigBaseDir(env = process.env, home = homedir(), platform = process.platform) {
|
|
10663
|
+
if (platform === "win32") return env.APPDATA && win32.isAbsolute(env.APPDATA) ? env.APPDATA : win32.join(home, "AppData", "Roaming");
|
|
10664
|
+
return env.XDG_CONFIG_HOME && posix.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : posix.join(home, ".config");
|
|
10665
|
+
}
|
|
10666
|
+
function defaultStateBaseDir(env = process.env, home = homedir(), platform = process.platform) {
|
|
10667
|
+
if (platform === "win32") return env.LOCALAPPDATA && win32.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : win32.join(home, "AppData", "Local");
|
|
10668
|
+
return env.XDG_STATE_HOME && posix.isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : posix.join(home, ".local", "state");
|
|
10669
|
+
}
|
|
10670
|
+
function defaultConfigPath(env = process.env, home = homedir(), platform = process.platform) {
|
|
10671
|
+
return (platform === "win32" ? win32.join : posix.join)(defaultConfigBaseDir(env, home, platform), "caplets", "config.json");
|
|
10672
|
+
}
|
|
10673
|
+
function defaultAuthDir(env = process.env, home = homedir(), platform = process.platform) {
|
|
10674
|
+
return (platform === "win32" ? win32.join : posix.join)(defaultStateBaseDir(env, home, platform), "caplets", "auth");
|
|
10675
|
+
}
|
|
10676
|
+
const DEFAULT_CONFIG_PATH = defaultConfigPath();
|
|
10677
|
+
const DEFAULT_AUTH_DIR = defaultAuthDir();
|
|
10678
|
+
const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
|
|
10679
|
+
const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
|
|
10680
|
+
function resolveConfigPath(path) {
|
|
10681
|
+
return path ?? DEFAULT_CONFIG_PATH;
|
|
10682
|
+
}
|
|
10683
|
+
function resolveProjectConfigPath(cwd = process.cwd()) {
|
|
10684
|
+
return join(cwd, PROJECT_CONFIG_FILE);
|
|
10685
|
+
}
|
|
10686
|
+
function resolveCapletsRoot(configPath = resolveConfigPath()) {
|
|
10687
|
+
return dirname(configPath);
|
|
10688
|
+
}
|
|
10689
|
+
function resolveProjectCapletsRoot(cwd = process.cwd()) {
|
|
10690
|
+
return join(cwd, ".caplets");
|
|
10691
|
+
}
|
|
10692
|
+
function isTrustedEnvEnabled(value) {
|
|
10693
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
10694
|
+
}
|
|
10695
|
+
//#endregion
|
|
10661
10696
|
//#region src/errors.ts
|
|
10662
10697
|
var CapletsError = class extends Error {
|
|
10663
10698
|
code;
|
|
@@ -10708,11 +10743,12 @@ function errorResult(error, fallback) {
|
|
|
10708
10743
|
}
|
|
10709
10744
|
//#endregion
|
|
10710
10745
|
//#region src/auth/store.ts
|
|
10711
|
-
function authStorePath(server, authDir =
|
|
10746
|
+
function authStorePath(server, authDir = DEFAULT_AUTH_DIR) {
|
|
10712
10747
|
if (!server || server.includes("/") || server.includes("\\") || server.includes("..")) throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
|
|
10713
10748
|
const authRoot = resolve(authDir);
|
|
10714
10749
|
const candidate = resolve(authRoot, `${server}.json`);
|
|
10715
|
-
|
|
10750
|
+
const relativePath = relative(authRoot, candidate);
|
|
10751
|
+
if (relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath)) return candidate;
|
|
10716
10752
|
throw new CapletsError("REQUEST_INVALID", `Invalid auth store server name ${server}`);
|
|
10717
10753
|
}
|
|
10718
10754
|
function readTokenBundle(server, authDir) {
|
|
@@ -10780,11 +10816,13 @@ var FileOAuthProvider = class {
|
|
|
10780
10816
|
verifier = base64url(randomBytes(32));
|
|
10781
10817
|
stateValue = base64url(randomBytes(24));
|
|
10782
10818
|
clientInfo;
|
|
10819
|
+
clientMetadataUrl;
|
|
10783
10820
|
constructor(server, redirectUrl, onRedirect, authDir) {
|
|
10784
10821
|
this.server = server;
|
|
10785
10822
|
this.redirectUrl = redirectUrl;
|
|
10786
10823
|
this.onRedirect = onRedirect;
|
|
10787
10824
|
this.authDir = authDir;
|
|
10825
|
+
if ((this.server.auth?.type === "oauth2" || this.server.auth?.type === "oidc") && this.server.auth.clientMetadataUrl) this.clientMetadataUrl = this.server.auth.clientMetadataUrl;
|
|
10788
10826
|
}
|
|
10789
10827
|
get clientMetadata() {
|
|
10790
10828
|
return {
|
|
@@ -10884,10 +10922,19 @@ async function runOAuthFlow(server, options = {}) {
|
|
|
10884
10922
|
authorizationCode: completion.code,
|
|
10885
10923
|
...scope ? { scope } : {}
|
|
10886
10924
|
});
|
|
10925
|
+
} catch (error) {
|
|
10926
|
+
throw normalizeMcpOAuthError(server, error);
|
|
10887
10927
|
} finally {
|
|
10888
10928
|
await callback.close();
|
|
10889
10929
|
}
|
|
10890
10930
|
}
|
|
10931
|
+
function normalizeMcpOAuthError(server, error) {
|
|
10932
|
+
if ((server.auth?.type === "oauth2" || server.auth?.type === "oidc") && !server.auth.clientId && !server.auth.clientMetadataUrl && error instanceof Error && error.message.includes("does not support dynamic client registration")) return new CapletsError("AUTH_FAILED", "OAuth is not available for this server without a host-specific OAuth app or PAT auth", {
|
|
10933
|
+
server: server.server,
|
|
10934
|
+
nextAction: "configure_bearer_auth_or_host_oauth_app"
|
|
10935
|
+
});
|
|
10936
|
+
return error;
|
|
10937
|
+
}
|
|
10891
10938
|
async function runGenericOAuthFlow(target, options = {}) {
|
|
10892
10939
|
if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
|
|
10893
10940
|
const authConfig = target.auth;
|
|
@@ -11057,6 +11104,11 @@ async function resolveGenericClient(target, authConfig, metadata, redirectUri, a
|
|
|
11057
11104
|
...authConfig.clientSecret ? { clientSecret: authConfig.clientSecret } : {},
|
|
11058
11105
|
dynamic: false
|
|
11059
11106
|
};
|
|
11107
|
+
if (authConfig.clientMetadataUrl) return {
|
|
11108
|
+
clientId: authConfig.clientMetadataUrl,
|
|
11109
|
+
...authConfig.clientSecret ? { clientSecret: authConfig.clientSecret } : {},
|
|
11110
|
+
dynamic: false
|
|
11111
|
+
};
|
|
11060
11112
|
if (!metadata.registration_endpoint) throw new CapletsError("AUTH_FAILED", "OAuth clientId is required without dynamic registration", { server: target.server });
|
|
11061
11113
|
const response = await fetchJson(metadata.registration_endpoint, target.requestTimeoutMs, {
|
|
11062
11114
|
method: "POST",
|
|
@@ -11125,8 +11177,9 @@ function assertAllowedAuthUrl(value, label, allowLoopbackHttp = false) {
|
|
|
11125
11177
|
throw new CapletsError("AUTH_FAILED", `${label} must use https except loopback development URLs`);
|
|
11126
11178
|
}
|
|
11127
11179
|
function assertTokenBundleMatchesTarget(bundle, target, authConfig) {
|
|
11180
|
+
const configuredClientId = authConfig.clientId ?? authConfig.clientMetadataUrl;
|
|
11128
11181
|
const expectedOrigin = protectedResourceOrigin(target, authConfig);
|
|
11129
|
-
if (bundle.authType !== authConfig.type || expectedOrigin && bundle.protectedResourceOrigin !== expectedOrigin ||
|
|
11182
|
+
if (bundle.authType !== authConfig.type || expectedOrigin && bundle.protectedResourceOrigin !== expectedOrigin || configuredClientId && bundle.clientId !== configuredClientId || authConfig.issuer && bundle.issuer !== authConfig.issuer) throw new CapletsError("AUTH_REQUIRED", `OAuth credentials for ${target.server} do not match the configured backend`, {
|
|
11130
11183
|
server: target.server,
|
|
11131
11184
|
backend: target.backend,
|
|
11132
11185
|
authType: authConfig.type,
|
|
@@ -18680,6 +18733,7 @@ function matter(file, options) {
|
|
|
18680
18733
|
//#region src/config/validation.ts
|
|
18681
18734
|
const SERVER_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
18682
18735
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
18736
|
+
const HTTP_BASE_URL_PATTERN = /^(?![a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*@)[^?#]*$/;
|
|
18683
18737
|
const FORBIDDEN_HEADERS = new Set([
|
|
18684
18738
|
"accept",
|
|
18685
18739
|
"authorization",
|
|
@@ -18697,6 +18751,16 @@ const FORBIDDEN_HEADERS = new Set([
|
|
|
18697
18751
|
"transfer-encoding",
|
|
18698
18752
|
"upgrade"
|
|
18699
18753
|
]);
|
|
18754
|
+
function validateHttpActionHeaders(headers, ctx, path) {
|
|
18755
|
+
for (const headerName of Object.keys(headers)) {
|
|
18756
|
+
const normalized = headerName.toLowerCase();
|
|
18757
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || FORBIDDEN_HEADERS.has(normalized)) ctx.addIssue({
|
|
18758
|
+
code: "custom",
|
|
18759
|
+
path: [...path, headerName],
|
|
18760
|
+
message: `header ${headerName} is not allowed`
|
|
18761
|
+
});
|
|
18762
|
+
}
|
|
18763
|
+
}
|
|
18700
18764
|
function isAllowedRemoteUrl(value) {
|
|
18701
18765
|
let url;
|
|
18702
18766
|
try {
|
|
@@ -18712,6 +18776,23 @@ function isAllowedRemoteUrl(value) {
|
|
|
18712
18776
|
"::1"
|
|
18713
18777
|
].includes(url.hostname);
|
|
18714
18778
|
}
|
|
18779
|
+
function isAllowedHttpBaseUrl(value) {
|
|
18780
|
+
let url;
|
|
18781
|
+
try {
|
|
18782
|
+
url = new URL(value);
|
|
18783
|
+
} catch {
|
|
18784
|
+
return false;
|
|
18785
|
+
}
|
|
18786
|
+
return isAllowedRemoteUrl(value) && !url.username && !url.password && !url.search && !url.hash;
|
|
18787
|
+
}
|
|
18788
|
+
function isUrl(value) {
|
|
18789
|
+
try {
|
|
18790
|
+
new URL(value);
|
|
18791
|
+
return true;
|
|
18792
|
+
} catch {
|
|
18793
|
+
return false;
|
|
18794
|
+
}
|
|
18795
|
+
}
|
|
18715
18796
|
//#endregion
|
|
18716
18797
|
//#region src/caplet-files.ts
|
|
18717
18798
|
const MAX_CAPLET_FILE_BYTES = 128 * 1024;
|
|
@@ -18734,6 +18815,7 @@ const capletRemoteAuthSchema = discriminatedUnion("type", [
|
|
|
18734
18815
|
resourceMetadataUrl: string().min(1).optional(),
|
|
18735
18816
|
authorizationServerMetadataUrl: string().min(1).optional(),
|
|
18736
18817
|
openidConfigurationUrl: string().min(1).optional(),
|
|
18818
|
+
clientMetadataUrl: string().min(1).optional(),
|
|
18737
18819
|
clientId: string().min(1).optional(),
|
|
18738
18820
|
clientSecret: string().min(1).optional(),
|
|
18739
18821
|
scopes: array(string().min(1)).optional(),
|
|
@@ -18747,6 +18829,7 @@ const capletRemoteAuthSchema = discriminatedUnion("type", [
|
|
|
18747
18829
|
resourceMetadataUrl: string().min(1).optional(),
|
|
18748
18830
|
authorizationServerMetadataUrl: string().min(1).optional(),
|
|
18749
18831
|
openidConfigurationUrl: string().min(1).optional(),
|
|
18832
|
+
clientMetadataUrl: string().min(1).optional(),
|
|
18750
18833
|
clientId: string().min(1).optional(),
|
|
18751
18834
|
clientSecret: string().min(1).optional(),
|
|
18752
18835
|
scopes: array(string().min(1)).optional(),
|
|
@@ -18771,6 +18854,7 @@ const capletEndpointAuthSchema = discriminatedUnion("type", [
|
|
|
18771
18854
|
resourceMetadataUrl: string().min(1).optional(),
|
|
18772
18855
|
authorizationServerMetadataUrl: string().min(1).optional(),
|
|
18773
18856
|
openidConfigurationUrl: string().min(1).optional(),
|
|
18857
|
+
clientMetadataUrl: string().min(1).optional(),
|
|
18774
18858
|
clientId: string().min(1).optional(),
|
|
18775
18859
|
clientSecret: string().min(1).optional(),
|
|
18776
18860
|
scopes: array(string().min(1)).optional(),
|
|
@@ -18784,6 +18868,7 @@ const capletEndpointAuthSchema = discriminatedUnion("type", [
|
|
|
18784
18868
|
resourceMetadataUrl: string().min(1).optional(),
|
|
18785
18869
|
authorizationServerMetadataUrl: string().min(1).optional(),
|
|
18786
18870
|
openidConfigurationUrl: string().min(1).optional(),
|
|
18871
|
+
clientMetadataUrl: string().min(1).optional(),
|
|
18787
18872
|
clientId: string().min(1).optional(),
|
|
18788
18873
|
clientSecret: string().min(1).optional(),
|
|
18789
18874
|
scopes: array(string().min(1)).optional(),
|
|
@@ -18827,10 +18912,11 @@ const capletMcpServerSchema = object$1({
|
|
|
18827
18912
|
path: ["url"],
|
|
18828
18913
|
message: "remote url must use https except loopback development urls"
|
|
18829
18914
|
});
|
|
18830
|
-
if (server.auth?.type === "oauth2") for (const field of [
|
|
18915
|
+
if (server.auth?.type === "oauth2" || server.auth?.type === "oidc") for (const field of [
|
|
18831
18916
|
"authorizationUrl",
|
|
18832
18917
|
"tokenUrl",
|
|
18833
18918
|
"issuer",
|
|
18919
|
+
"clientMetadataUrl",
|
|
18834
18920
|
"redirectUri"
|
|
18835
18921
|
]) {
|
|
18836
18922
|
const value = server.auth[field];
|
|
@@ -18917,6 +19003,52 @@ const capletGraphQlEndpointSchema = object$1({
|
|
|
18917
19003
|
});
|
|
18918
19004
|
validateEndpointAuthHeaders$1(endpoint.auth, ctx);
|
|
18919
19005
|
});
|
|
19006
|
+
const httpScalarMappingSchema$1 = record(string(), union([
|
|
19007
|
+
string(),
|
|
19008
|
+
number$1(),
|
|
19009
|
+
boolean()
|
|
19010
|
+
]));
|
|
19011
|
+
const capletHttpActionSchema = object$1({
|
|
19012
|
+
method: _enum([
|
|
19013
|
+
"GET",
|
|
19014
|
+
"POST",
|
|
19015
|
+
"PUT",
|
|
19016
|
+
"PATCH",
|
|
19017
|
+
"DELETE"
|
|
19018
|
+
]).describe("HTTP method used for this action."),
|
|
19019
|
+
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"),
|
|
19020
|
+
description: string().min(1).optional().describe("Action capability description."),
|
|
19021
|
+
inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
|
|
19022
|
+
query: httpScalarMappingSchema$1.optional().describe("Query parameter mapping."),
|
|
19023
|
+
headers: httpScalarMappingSchema$1.optional().describe("Request header mapping."),
|
|
19024
|
+
jsonBody: unknown().optional().describe("JSON request body mapping.")
|
|
19025
|
+
}).strict().superRefine((action, ctx) => {
|
|
19026
|
+
if (action.method === "GET" && action.jsonBody !== void 0) ctx.addIssue({
|
|
19027
|
+
code: "custom",
|
|
19028
|
+
path: ["jsonBody"],
|
|
19029
|
+
message: "HTTP GET actions must not define jsonBody"
|
|
19030
|
+
});
|
|
19031
|
+
});
|
|
19032
|
+
const capletHttpApiSchema = object$1({
|
|
19033
|
+
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."),
|
|
19034
|
+
auth: capletEndpointAuthSchema.describe("Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs."),
|
|
19035
|
+
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."),
|
|
19036
|
+
requestTimeoutMs: number$1().int().positive().optional().describe("Timeout in milliseconds for HTTP action requests."),
|
|
19037
|
+
maxResponseBytes: number$1().int().positive().optional().describe("Maximum HTTP action response body bytes to read."),
|
|
19038
|
+
disabled: boolean().optional().describe("When true, omit this Caplet from discovery.")
|
|
19039
|
+
}).strict().superRefine((api, ctx) => {
|
|
19040
|
+
if (api.baseUrl && !hasEnvReference$1(api.baseUrl) && !isAllowedHttpBaseUrl(api.baseUrl)) ctx.addIssue({
|
|
19041
|
+
code: "custom",
|
|
19042
|
+
path: ["baseUrl"],
|
|
19043
|
+
message: "HTTP API baseUrl must use https except loopback development urls and must not include credentials, query, or fragment"
|
|
19044
|
+
});
|
|
19045
|
+
validateEndpointAuthHeaders$1(api.auth, ctx);
|
|
19046
|
+
for (const [actionName, action] of Object.entries(api.actions)) if (action.headers) validateHttpActionHeaders(action.headers, ctx, [
|
|
19047
|
+
"actions",
|
|
19048
|
+
actionName,
|
|
19049
|
+
"headers"
|
|
19050
|
+
]);
|
|
19051
|
+
});
|
|
18920
19052
|
const capletFileSchema = object$1({
|
|
18921
19053
|
$schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
|
|
18922
19054
|
name: string().trim().min(1).max(80).describe("Human-readable Caplet display name."),
|
|
@@ -18924,11 +19056,12 @@ const capletFileSchema = object$1({
|
|
|
18924
19056
|
tags: array(string().trim().min(1).max(80)).optional().describe("Optional tags for grouping or searching Caplets."),
|
|
18925
19057
|
mcpServer: capletMcpServerSchema.describe("MCP server backend configuration for this Caplet.").optional(),
|
|
18926
19058
|
openapiEndpoint: capletOpenApiEndpointSchema.describe("OpenAPI endpoint backend configuration for this Caplet.").optional(),
|
|
18927
|
-
graphqlEndpoint: capletGraphQlEndpointSchema.describe("GraphQL endpoint backend configuration for this Caplet.").optional()
|
|
19059
|
+
graphqlEndpoint: capletGraphQlEndpointSchema.describe("GraphQL endpoint backend configuration for this Caplet.").optional(),
|
|
19060
|
+
httpApi: capletHttpApiSchema.describe("HTTP API backend configuration for this Caplet.").optional()
|
|
18928
19061
|
}).strict().superRefine((frontmatter, ctx) => {
|
|
18929
|
-
if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) !== 1) ctx.addIssue({
|
|
19062
|
+
if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) + Number(Boolean(frontmatter.httpApi)) !== 1) ctx.addIssue({
|
|
18930
19063
|
code: "custom",
|
|
18931
|
-
message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, or
|
|
19064
|
+
message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, graphqlEndpoint, or httpApi"
|
|
18932
19065
|
});
|
|
18933
19066
|
});
|
|
18934
19067
|
function loadCapletFiles(root) {
|
|
@@ -18936,24 +19069,30 @@ function loadCapletFiles(root) {
|
|
|
18936
19069
|
const servers = {};
|
|
18937
19070
|
const openapiEndpoints = {};
|
|
18938
19071
|
const graphqlEndpoints = {};
|
|
19072
|
+
const httpApis = {};
|
|
18939
19073
|
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}`);
|
|
19074
|
+
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
19075
|
const config = readCapletFile(candidate.path);
|
|
18942
|
-
if (isPlainObject$
|
|
19076
|
+
if (isPlainObject$4(config) && config.backend === "openapi") {
|
|
18943
19077
|
const { backend: _backend, ...endpoint } = config;
|
|
18944
19078
|
openapiEndpoints[candidate.id] = endpoint;
|
|
18945
|
-
} else if (isPlainObject$
|
|
19079
|
+
} else if (isPlainObject$4(config) && config.backend === "graphql") {
|
|
18946
19080
|
const { backend: _backend, ...endpoint } = config;
|
|
18947
19081
|
graphqlEndpoints[candidate.id] = endpoint;
|
|
19082
|
+
} else if (isPlainObject$4(config) && config.backend === "http") {
|
|
19083
|
+
const { backend: _backend, ...endpoint } = config;
|
|
19084
|
+
httpApis[candidate.id] = endpoint;
|
|
18948
19085
|
} else servers[candidate.id] = config;
|
|
18949
19086
|
}
|
|
18950
19087
|
const hasServers = Object.keys(servers).length > 0;
|
|
18951
19088
|
const hasOpenApi = Object.keys(openapiEndpoints).length > 0;
|
|
18952
19089
|
const hasGraphQl = Object.keys(graphqlEndpoints).length > 0;
|
|
18953
|
-
|
|
19090
|
+
const hasHttpApis = Object.keys(httpApis).length > 0;
|
|
19091
|
+
return hasServers || hasOpenApi || hasGraphQl || hasHttpApis ? {
|
|
18954
19092
|
...hasServers ? { mcpServers: servers } : {},
|
|
18955
19093
|
...hasOpenApi ? { openapiEndpoints } : {},
|
|
18956
|
-
...hasGraphQl ? { graphqlEndpoints } : {}
|
|
19094
|
+
...hasGraphQl ? { graphqlEndpoints } : {},
|
|
19095
|
+
...hasHttpApis ? { httpApis } : {}
|
|
18957
19096
|
} : void 0;
|
|
18958
19097
|
}
|
|
18959
19098
|
function discoverCapletFiles(root) {
|
|
@@ -19011,6 +19150,14 @@ function capletToServerConfig(frontmatter, body, baseDir) {
|
|
|
19011
19150
|
...frontmatter.tags ? { tags: frontmatter.tags } : {},
|
|
19012
19151
|
body
|
|
19013
19152
|
};
|
|
19153
|
+
if (frontmatter.httpApi) return {
|
|
19154
|
+
...frontmatter.httpApi,
|
|
19155
|
+
backend: "http",
|
|
19156
|
+
name: frontmatter.name,
|
|
19157
|
+
description: frontmatter.description,
|
|
19158
|
+
...frontmatter.tags ? { tags: frontmatter.tags } : {},
|
|
19159
|
+
body
|
|
19160
|
+
};
|
|
19014
19161
|
return {
|
|
19015
19162
|
...frontmatter.mcpServer,
|
|
19016
19163
|
name: frontmatter.name,
|
|
@@ -19053,7 +19200,7 @@ function parseFrontmatter(text, path) {
|
|
|
19053
19200
|
value: text
|
|
19054
19201
|
});
|
|
19055
19202
|
matter(file, { strip: true });
|
|
19056
|
-
if (!isPlainObject$
|
|
19203
|
+
if (!isPlainObject$4(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
|
|
19057
19204
|
return {
|
|
19058
19205
|
frontmatter: file.data.matter,
|
|
19059
19206
|
body: String(file)
|
|
@@ -19062,7 +19209,7 @@ function parseFrontmatter(text, path) {
|
|
|
19062
19209
|
throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid YAML frontmatter`, redactSecrets(error));
|
|
19063
19210
|
}
|
|
19064
19211
|
}
|
|
19065
|
-
function isPlainObject$
|
|
19212
|
+
function isPlainObject$4(value) {
|
|
19066
19213
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19067
19214
|
}
|
|
19068
19215
|
function validateCapletId(id, path) {
|
|
@@ -19071,35 +19218,6 @@ function validateCapletId(id, path) {
|
|
|
19071
19218
|
function hasEnvReference$1(value) {
|
|
19072
19219
|
return /\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$env:[A-Za-z_][A-Za-z0-9_]*/.test(value);
|
|
19073
19220
|
}
|
|
19074
|
-
function isUrl(value) {
|
|
19075
|
-
try {
|
|
19076
|
-
new URL(value);
|
|
19077
|
-
return true;
|
|
19078
|
-
} catch {
|
|
19079
|
-
return false;
|
|
19080
|
-
}
|
|
19081
|
-
}
|
|
19082
|
-
//#endregion
|
|
19083
|
-
//#region src/config/paths.ts
|
|
19084
|
-
const DEFAULT_CONFIG_PATH = join(homedir(), ".caplets", "config.json");
|
|
19085
|
-
const DEFAULT_AUTH_DIR = join(homedir(), ".caplets", "auth");
|
|
19086
|
-
const PROJECT_CONFIG_FILE = join(".caplets", "config.json");
|
|
19087
|
-
const TRUST_PROJECT_CAPLETS_ENV = "CAPLETS_TRUST_PROJECT_CAPLETS";
|
|
19088
|
-
function resolveConfigPath(path) {
|
|
19089
|
-
return path ?? DEFAULT_CONFIG_PATH;
|
|
19090
|
-
}
|
|
19091
|
-
function resolveProjectConfigPath(cwd = process.cwd()) {
|
|
19092
|
-
return join(cwd, PROJECT_CONFIG_FILE);
|
|
19093
|
-
}
|
|
19094
|
-
function resolveCapletsRoot(configPath = resolveConfigPath()) {
|
|
19095
|
-
return dirname(configPath);
|
|
19096
|
-
}
|
|
19097
|
-
function resolveProjectCapletsRoot(cwd = process.cwd()) {
|
|
19098
|
-
return join(cwd, ".caplets");
|
|
19099
|
-
}
|
|
19100
|
-
function isTrustedEnvEnabled(value) {
|
|
19101
|
-
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
19102
|
-
}
|
|
19103
19221
|
//#endregion
|
|
19104
19222
|
//#region src/config.ts
|
|
19105
19223
|
const NON_INTERPOLATED_SERVER_FIELDS = new Set([
|
|
@@ -19126,6 +19244,7 @@ const remoteAuthSchema = discriminatedUnion("type", [
|
|
|
19126
19244
|
resourceMetadataUrl: string().url().optional(),
|
|
19127
19245
|
authorizationServerMetadataUrl: string().url().optional(),
|
|
19128
19246
|
openidConfigurationUrl: string().url().optional(),
|
|
19247
|
+
clientMetadataUrl: string().url().optional(),
|
|
19129
19248
|
clientId: string().min(1).optional(),
|
|
19130
19249
|
clientSecret: string().min(1).optional(),
|
|
19131
19250
|
scopes: array(string().min(1)).optional(),
|
|
@@ -19139,6 +19258,7 @@ const remoteAuthSchema = discriminatedUnion("type", [
|
|
|
19139
19258
|
resourceMetadataUrl: string().url().optional(),
|
|
19140
19259
|
authorizationServerMetadataUrl: string().url().optional(),
|
|
19141
19260
|
openidConfigurationUrl: string().url().optional(),
|
|
19261
|
+
clientMetadataUrl: string().url().optional(),
|
|
19142
19262
|
clientId: string().min(1).optional(),
|
|
19143
19263
|
clientSecret: string().min(1).optional(),
|
|
19144
19264
|
scopes: array(string().min(1)).optional(),
|
|
@@ -19153,6 +19273,7 @@ const oauthLikeAuthSchema = union([object$1({
|
|
|
19153
19273
|
resourceMetadataUrl: string().url().optional(),
|
|
19154
19274
|
authorizationServerMetadataUrl: string().url().optional(),
|
|
19155
19275
|
openidConfigurationUrl: string().url().optional(),
|
|
19276
|
+
clientMetadataUrl: string().url().optional(),
|
|
19156
19277
|
clientId: string().min(1).optional(),
|
|
19157
19278
|
clientSecret: string().min(1).optional(),
|
|
19158
19279
|
scopes: array(string().min(1)).optional(),
|
|
@@ -19165,6 +19286,7 @@ const oauthLikeAuthSchema = union([object$1({
|
|
|
19165
19286
|
resourceMetadataUrl: string().url().optional(),
|
|
19166
19287
|
authorizationServerMetadataUrl: string().url().optional(),
|
|
19167
19288
|
openidConfigurationUrl: string().url().optional(),
|
|
19289
|
+
clientMetadataUrl: string().url().optional(),
|
|
19168
19290
|
clientId: string().min(1).optional(),
|
|
19169
19291
|
clientSecret: string().min(1).optional(),
|
|
19170
19292
|
scopes: array(string().min(1)).optional(),
|
|
@@ -19248,7 +19370,45 @@ const publicGraphQlEndpointSchema = object$1({
|
|
|
19248
19370
|
});
|
|
19249
19371
|
});
|
|
19250
19372
|
const normalizedGraphQlEndpointSchema = publicGraphQlEndpointSchema.extend({ body: string().optional() });
|
|
19251
|
-
|
|
19373
|
+
const httpScalarMappingSchema = record(string(), union([
|
|
19374
|
+
string(),
|
|
19375
|
+
number$1(),
|
|
19376
|
+
boolean()
|
|
19377
|
+
]));
|
|
19378
|
+
const httpActionSchema = object$1({
|
|
19379
|
+
method: _enum([
|
|
19380
|
+
"GET",
|
|
19381
|
+
"POST",
|
|
19382
|
+
"PUT",
|
|
19383
|
+
"PATCH",
|
|
19384
|
+
"DELETE"
|
|
19385
|
+
]).describe("HTTP method used for this action."),
|
|
19386
|
+
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"),
|
|
19387
|
+
description: string().min(1).optional().describe("Action capability description."),
|
|
19388
|
+
inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
|
|
19389
|
+
query: httpScalarMappingSchema.optional().describe("Query parameter mapping."),
|
|
19390
|
+
headers: httpScalarMappingSchema.optional().describe("Request header mapping."),
|
|
19391
|
+
jsonBody: unknown().optional().describe("JSON request body mapping.")
|
|
19392
|
+
}).strict().superRefine((action, ctx) => {
|
|
19393
|
+
if (action.method === "GET" && action.jsonBody !== void 0) ctx.addIssue({
|
|
19394
|
+
code: "custom",
|
|
19395
|
+
path: ["jsonBody"],
|
|
19396
|
+
message: "HTTP GET actions must not define jsonBody"
|
|
19397
|
+
});
|
|
19398
|
+
});
|
|
19399
|
+
const publicHttpApiSchema = object$1({
|
|
19400
|
+
name: string().trim().min(1).max(80).describe("Human-readable HTTP API display name."),
|
|
19401
|
+
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"),
|
|
19402
|
+
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."),
|
|
19403
|
+
auth: openApiAuthSchema.describe("Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs."),
|
|
19404
|
+
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."),
|
|
19405
|
+
tags: array(string().trim().min(1).max(80)).optional(),
|
|
19406
|
+
requestTimeoutMs: number$1().int().positive().default(6e4).describe("Timeout in milliseconds for HTTP action requests."),
|
|
19407
|
+
maxResponseBytes: number$1().int().positive().default(1e6).describe("Maximum HTTP action response body bytes to read."),
|
|
19408
|
+
disabled: boolean().default(false).describe("When true, omit this HTTP API Caplet.")
|
|
19409
|
+
}).strict();
|
|
19410
|
+
const normalizedHttpApiSchema = publicHttpApiSchema.extend({ body: string().optional() });
|
|
19411
|
+
function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlEndpointValueSchema, httpApiValueSchema) {
|
|
19252
19412
|
return object$1({
|
|
19253
19413
|
$schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
|
|
19254
19414
|
version: literal(1).default(1).describe("Caplets config schema version."),
|
|
@@ -19256,7 +19416,8 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
|
|
|
19256
19416
|
maxSearchLimit: number$1().int().positive().max(50).default(50).describe("Maximum accepted search_tools limit."),
|
|
19257
19417
|
mcpServers: record(string().regex(SERVER_ID_PATTERN), serverValueSchema).default({}).describe("Downstream MCP servers keyed by stable server ID."),
|
|
19258
19418
|
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.")
|
|
19419
|
+
graphqlEndpoints: record(string().regex(SERVER_ID_PATTERN), graphQlEndpointValueSchema).default({}).describe("GraphQL endpoints keyed by stable Caplet ID."),
|
|
19420
|
+
httpApis: record(string().regex(SERVER_ID_PATTERN), httpApiValueSchema).default({}).describe("HTTP APIs keyed by stable Caplet ID.")
|
|
19260
19421
|
}).strict().superRefine((config, ctx) => {
|
|
19261
19422
|
if (config.defaultSearchLimit > config.maxSearchLimit) ctx.addIssue({
|
|
19262
19423
|
code: "custom",
|
|
@@ -19410,10 +19571,45 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
|
|
|
19410
19571
|
"auth"
|
|
19411
19572
|
]);
|
|
19412
19573
|
}
|
|
19574
|
+
for (const [endpoint, rawValue] of Object.entries(config.httpApis)) {
|
|
19575
|
+
const raw = rawValue;
|
|
19576
|
+
const duplicateBackend = config.mcpServers[endpoint] ? "mcpServers" : config.openapiEndpoints[endpoint] ? "openapiEndpoints" : config.graphqlEndpoints[endpoint] ? "graphqlEndpoints" : void 0;
|
|
19577
|
+
if (duplicateBackend) ctx.addIssue({
|
|
19578
|
+
code: "custom",
|
|
19579
|
+
path: ["httpApis", endpoint],
|
|
19580
|
+
message: `Caplet ID ${endpoint} is already used by ${duplicateBackend}`
|
|
19581
|
+
});
|
|
19582
|
+
if (!SERVER_ID_PATTERN.test(endpoint)) ctx.addIssue({
|
|
19583
|
+
code: "custom",
|
|
19584
|
+
path: ["httpApis", endpoint],
|
|
19585
|
+
message: "HTTP API ID must match ^[a-zA-Z0-9_-]{1,64}$"
|
|
19586
|
+
});
|
|
19587
|
+
if (raw.baseUrl && !isAllowedHttpBaseUrl(raw.baseUrl)) ctx.addIssue({
|
|
19588
|
+
code: "custom",
|
|
19589
|
+
path: [
|
|
19590
|
+
"httpApis",
|
|
19591
|
+
endpoint,
|
|
19592
|
+
"baseUrl"
|
|
19593
|
+
],
|
|
19594
|
+
message: "HTTP API baseUrl must use https except loopback development urls and must not include credentials, query, or fragment"
|
|
19595
|
+
});
|
|
19596
|
+
validateEndpointAuthHeaders(raw.auth, ctx, [
|
|
19597
|
+
"httpApis",
|
|
19598
|
+
endpoint,
|
|
19599
|
+
"auth"
|
|
19600
|
+
]);
|
|
19601
|
+
for (const [actionName, action] of Object.entries(raw.actions)) if (action.headers) validateHttpActionHeaders(action.headers, ctx, [
|
|
19602
|
+
"httpApis",
|
|
19603
|
+
endpoint,
|
|
19604
|
+
"actions",
|
|
19605
|
+
actionName,
|
|
19606
|
+
"headers"
|
|
19607
|
+
]);
|
|
19608
|
+
}
|
|
19413
19609
|
});
|
|
19414
19610
|
}
|
|
19415
|
-
const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema);
|
|
19416
|
-
const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema);
|
|
19611
|
+
const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema, publicHttpApiSchema);
|
|
19612
|
+
const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema, normalizedHttpApiSchema);
|
|
19417
19613
|
function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConfigPath()) {
|
|
19418
19614
|
const hasUserConfig = existsSync(path);
|
|
19419
19615
|
const hasProjectConfig = existsSync(projectPath);
|
|
@@ -19424,7 +19620,7 @@ function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConf
|
|
|
19424
19620
|
if (!hasUserConfig && !hasProjectConfig && !userCaplets && !projectCaplets) throw new CapletsError("CONFIG_NOT_FOUND", `Caplets config not found at ${path} or ${projectPath}`);
|
|
19425
19621
|
try {
|
|
19426
19622
|
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
|
|
19623
|
+
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
19624
|
return config;
|
|
19429
19625
|
} catch (error) {
|
|
19430
19626
|
if (error instanceof CapletsError) throw error;
|
|
@@ -19454,7 +19650,7 @@ function normalizeLocalPaths(input, baseDir) {
|
|
|
19454
19650
|
}
|
|
19455
19651
|
function normalizeEndpointPaths(endpoints, baseDir, normalize) {
|
|
19456
19652
|
if (!endpoints) return;
|
|
19457
|
-
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$
|
|
19653
|
+
return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$3(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
|
|
19458
19654
|
}
|
|
19459
19655
|
function normalizeOpenApiPath(endpoint, baseDir) {
|
|
19460
19656
|
return {
|
|
@@ -19463,7 +19659,7 @@ function normalizeOpenApiPath(endpoint, baseDir) {
|
|
|
19463
19659
|
};
|
|
19464
19660
|
}
|
|
19465
19661
|
function normalizeGraphQlPath(endpoint, baseDir) {
|
|
19466
|
-
const operations = isPlainObject$
|
|
19662
|
+
const operations = isPlainObject$3(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$3(operation) ? {
|
|
19467
19663
|
...operation,
|
|
19468
19664
|
documentPath: normalizeLocalPath(operation.documentPath, baseDir)
|
|
19469
19665
|
} : operation])) : endpoint.operations;
|
|
@@ -19480,6 +19676,7 @@ function normalizeLocalPath(value, baseDir) {
|
|
|
19480
19676
|
function rejectUntrustedProjectExecutableBackends(input, path) {
|
|
19481
19677
|
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
19678
|
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`);
|
|
19679
|
+
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
19680
|
return input;
|
|
19484
19681
|
}
|
|
19485
19682
|
function mergeConfigInputs(...inputs) {
|
|
@@ -19500,6 +19697,10 @@ function mergeConfigInputs(...inputs) {
|
|
|
19500
19697
|
graphqlEndpoints: {
|
|
19501
19698
|
...merged?.graphqlEndpoints,
|
|
19502
19699
|
...input.graphqlEndpoints
|
|
19700
|
+
},
|
|
19701
|
+
httpApis: {
|
|
19702
|
+
...merged?.httpApis,
|
|
19703
|
+
...input.httpApis
|
|
19503
19704
|
}
|
|
19504
19705
|
};
|
|
19505
19706
|
}
|
|
@@ -19530,6 +19731,12 @@ function parseConfig(input) {
|
|
|
19530
19731
|
server,
|
|
19531
19732
|
backend: "graphql"
|
|
19532
19733
|
});
|
|
19734
|
+
const httpApis = {};
|
|
19735
|
+
for (const [server, raw] of Object.entries(parsed.data.httpApis)) httpApis[server] = stripUndefined({
|
|
19736
|
+
...raw,
|
|
19737
|
+
server,
|
|
19738
|
+
backend: "http"
|
|
19739
|
+
});
|
|
19533
19740
|
return {
|
|
19534
19741
|
version: parsed.data.version,
|
|
19535
19742
|
options: {
|
|
@@ -19538,7 +19745,8 @@ function parseConfig(input) {
|
|
|
19538
19745
|
},
|
|
19539
19746
|
mcpServers: servers,
|
|
19540
19747
|
openapiEndpoints,
|
|
19541
|
-
graphqlEndpoints
|
|
19748
|
+
graphqlEndpoints,
|
|
19749
|
+
httpApis
|
|
19542
19750
|
};
|
|
19543
19751
|
}
|
|
19544
19752
|
function validateEndpointAuthHeaders(auth, ctx, path) {
|
|
@@ -19567,10 +19775,10 @@ function interpolateConfig(value, path = []) {
|
|
|
19567
19775
|
return value;
|
|
19568
19776
|
}
|
|
19569
19777
|
function isPublicMetadataPath(path) {
|
|
19570
|
-
if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints") return false;
|
|
19778
|
+
if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis") return false;
|
|
19571
19779
|
return NON_INTERPOLATED_SERVER_FIELDS.has(path[2] ?? "");
|
|
19572
19780
|
}
|
|
19573
|
-
function isPlainObject$
|
|
19781
|
+
function isPlainObject$3(value) {
|
|
19574
19782
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19575
19783
|
}
|
|
19576
19784
|
function hasEnvReference(value) {
|
|
@@ -19629,7 +19837,8 @@ function authTargets(config) {
|
|
|
19629
19837
|
return [
|
|
19630
19838
|
...Object.values(config.mcpServers).filter((server) => server.transport !== "stdio" && (server.auth?.type === "oauth2" || server.auth?.type === "oidc")),
|
|
19631
19839
|
...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)
|
|
19840
|
+
...Object.values(config.graphqlEndpoints).filter((endpoint) => endpoint.auth?.type === "oauth2" || endpoint.auth?.type === "oidc").map(graphQlAuthTarget),
|
|
19841
|
+
...Object.values(config.httpApis).filter((api) => api.auth?.type === "oauth2" || api.auth?.type === "oidc").map(httpAuthTarget)
|
|
19633
19842
|
];
|
|
19634
19843
|
}
|
|
19635
19844
|
function graphQlAuthTarget(endpoint) {
|
|
@@ -19638,6 +19847,9 @@ function graphQlAuthTarget(endpoint) {
|
|
|
19638
19847
|
url: endpoint.endpointUrl
|
|
19639
19848
|
};
|
|
19640
19849
|
}
|
|
19850
|
+
function httpAuthTarget(api) {
|
|
19851
|
+
return { ...api };
|
|
19852
|
+
}
|
|
19641
19853
|
function assertLoginTarget(target, serverId) {
|
|
19642
19854
|
if (!target) throw new CapletsError("SERVER_NOT_FOUND", `Server ${serverId} is not configured for OAuth`);
|
|
19643
19855
|
if ("disabled" in target && target.disabled) throw new CapletsError("SERVER_UNAVAILABLE", `Server ${serverId} is disabled`);
|
|
@@ -19725,12 +19937,14 @@ function formatCapletList(rows) {
|
|
|
19725
19937
|
}
|
|
19726
19938
|
function resolveCliConfigPaths(envConfigPath, authDir) {
|
|
19727
19939
|
const configPath = resolveConfigPath(envConfigPath);
|
|
19940
|
+
const effectiveAuthDir = authDir ?? DEFAULT_AUTH_DIR;
|
|
19728
19941
|
return {
|
|
19729
19942
|
userConfig: configPath,
|
|
19730
19943
|
projectConfig: resolveProjectConfigPath(),
|
|
19731
19944
|
userRoot: resolveCapletsRoot(configPath),
|
|
19945
|
+
stateRoot: dirname(effectiveAuthDir),
|
|
19732
19946
|
projectRoot: resolveProjectCapletsRoot(),
|
|
19733
|
-
authDir:
|
|
19947
|
+
authDir: effectiveAuthDir,
|
|
19734
19948
|
envConfig: envConfigPath ?? null,
|
|
19735
19949
|
projectCapletsTrusted: isTrustedProjectCapletsEnabled()
|
|
19736
19950
|
};
|
|
@@ -19740,6 +19954,7 @@ function formatConfigPaths(paths) {
|
|
|
19740
19954
|
`userConfig: ${paths.userConfig}`,
|
|
19741
19955
|
`projectConfig: ${paths.projectConfig}`,
|
|
19742
19956
|
`userRoot: ${paths.userRoot}`,
|
|
19957
|
+
`stateRoot: ${paths.stateRoot}`,
|
|
19743
19958
|
`projectRoot: ${paths.projectRoot}`,
|
|
19744
19959
|
`authDir: ${paths.authDir}`,
|
|
19745
19960
|
`envConfig: ${paths.envConfig ?? "unset"}`,
|
|
@@ -25649,7 +25864,7 @@ var Protocol = class {
|
|
|
25649
25864
|
};
|
|
25650
25865
|
}
|
|
25651
25866
|
};
|
|
25652
|
-
function isPlainObject$
|
|
25867
|
+
function isPlainObject$2(value) {
|
|
25653
25868
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25654
25869
|
}
|
|
25655
25870
|
function mergeCapabilities(base, additional) {
|
|
@@ -25659,7 +25874,7 @@ function mergeCapabilities(base, additional) {
|
|
|
25659
25874
|
const addValue = additional[k];
|
|
25660
25875
|
if (addValue === void 0) continue;
|
|
25661
25876
|
const baseValue = result[k];
|
|
25662
|
-
if (isPlainObject$
|
|
25877
|
+
if (isPlainObject$2(baseValue) && isPlainObject$2(addValue)) result[k] = {
|
|
25663
25878
|
...baseValue,
|
|
25664
25879
|
...addValue
|
|
25665
25880
|
};
|
|
@@ -50843,6 +51058,279 @@ function graphQlCacheKey(endpoint) {
|
|
|
50843
51058
|
});
|
|
50844
51059
|
}
|
|
50845
51060
|
//#endregion
|
|
51061
|
+
//#region src/http-actions.ts
|
|
51062
|
+
const DEFAULT_INPUT_SCHEMA = {
|
|
51063
|
+
type: "object",
|
|
51064
|
+
additionalProperties: true
|
|
51065
|
+
};
|
|
51066
|
+
var HttpActionManager = class {
|
|
51067
|
+
registry;
|
|
51068
|
+
options;
|
|
51069
|
+
constructor(registry, options = {}) {
|
|
51070
|
+
this.registry = registry;
|
|
51071
|
+
this.options = options;
|
|
51072
|
+
}
|
|
51073
|
+
updateRegistry(registry) {
|
|
51074
|
+
this.registry = registry;
|
|
51075
|
+
}
|
|
51076
|
+
invalidate(_serverId) {}
|
|
51077
|
+
async checkApi(api) {
|
|
51078
|
+
const startedAt = Date.now();
|
|
51079
|
+
try {
|
|
51080
|
+
const operations = operationsFor(api);
|
|
51081
|
+
validateBaseUrl(api);
|
|
51082
|
+
authHeaders$1(api, this.options.authDir);
|
|
51083
|
+
for (const operation of operations) validateAction(api, operation);
|
|
51084
|
+
this.registry.setStatus(api.server, "available");
|
|
51085
|
+
return {
|
|
51086
|
+
server: api.server,
|
|
51087
|
+
status: "available",
|
|
51088
|
+
toolCount: operations.length,
|
|
51089
|
+
elapsedMs: Date.now() - startedAt
|
|
51090
|
+
};
|
|
51091
|
+
} catch (error) {
|
|
51092
|
+
const safe = toSafeError(error, "SERVER_UNAVAILABLE");
|
|
51093
|
+
this.registry.setStatus(api.server, "unavailable", safe);
|
|
51094
|
+
return {
|
|
51095
|
+
server: api.server,
|
|
51096
|
+
status: "unavailable",
|
|
51097
|
+
elapsedMs: Date.now() - startedAt,
|
|
51098
|
+
error: safe
|
|
51099
|
+
};
|
|
51100
|
+
}
|
|
51101
|
+
}
|
|
51102
|
+
async listTools(api) {
|
|
51103
|
+
return operationsFor(api).map((operation) => this.toTool(operation));
|
|
51104
|
+
}
|
|
51105
|
+
async getTool(api, toolName) {
|
|
51106
|
+
return this.toTool(getOperation(api, toolName));
|
|
51107
|
+
}
|
|
51108
|
+
async callTool(api, toolName, args) {
|
|
51109
|
+
const operation = getOperation(api, toolName);
|
|
51110
|
+
const startedAt = Date.now();
|
|
51111
|
+
const request = buildRequest$1(api, operation, args, this.options.authDir);
|
|
51112
|
+
const controller = new AbortController();
|
|
51113
|
+
const timeout = setTimeout(() => controller.abort(), api.requestTimeoutMs);
|
|
51114
|
+
try {
|
|
51115
|
+
const response = await fetch(request.url, {
|
|
51116
|
+
method: operation.method,
|
|
51117
|
+
headers: request.headers,
|
|
51118
|
+
redirect: "manual",
|
|
51119
|
+
signal: controller.signal,
|
|
51120
|
+
...request.body === void 0 ? {} : { body: request.body }
|
|
51121
|
+
});
|
|
51122
|
+
if (response.status >= 300 && response.status < 400) throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "HTTP action request returned a redirect", {
|
|
51123
|
+
server: api.server,
|
|
51124
|
+
status: response.status,
|
|
51125
|
+
location: response.headers.get("location") ? "[REDACTED]" : void 0
|
|
51126
|
+
});
|
|
51127
|
+
const parsed = await readResponse$1(response, api, Date.now() - startedAt);
|
|
51128
|
+
return {
|
|
51129
|
+
content: [{
|
|
51130
|
+
type: "text",
|
|
51131
|
+
text: JSON.stringify(parsed, null, 2)
|
|
51132
|
+
}],
|
|
51133
|
+
structuredContent: parsed,
|
|
51134
|
+
isError: !response.ok
|
|
51135
|
+
};
|
|
51136
|
+
} catch (error) {
|
|
51137
|
+
if (isAbortError(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `HTTP action request timed out for ${api.server}/${toolName}`);
|
|
51138
|
+
if (error instanceof CapletsError) throw error;
|
|
51139
|
+
throw new CapletsError("DOWNSTREAM_TOOL_ERROR", `HTTP action request failed for ${api.server}/${toolName}`, toSafeError(error));
|
|
51140
|
+
} finally {
|
|
51141
|
+
clearTimeout(timeout);
|
|
51142
|
+
}
|
|
51143
|
+
}
|
|
51144
|
+
compact(api, tool) {
|
|
51145
|
+
return {
|
|
51146
|
+
server: api.server,
|
|
51147
|
+
tool: tool.name,
|
|
51148
|
+
...tool.description ? { description: tool.description } : {},
|
|
51149
|
+
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
51150
|
+
hasInputSchema: Boolean(tool.inputSchema)
|
|
51151
|
+
};
|
|
51152
|
+
}
|
|
51153
|
+
search(api, tools, query, limit) {
|
|
51154
|
+
const needle = query.toLocaleLowerCase();
|
|
51155
|
+
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));
|
|
51156
|
+
}
|
|
51157
|
+
toTool(operation) {
|
|
51158
|
+
return {
|
|
51159
|
+
name: operation.name,
|
|
51160
|
+
...operation.description ? { description: operation.description } : {},
|
|
51161
|
+
inputSchema: operation.inputSchema ?? DEFAULT_INPUT_SCHEMA,
|
|
51162
|
+
annotations: {
|
|
51163
|
+
readOnlyHint: operation.method === "GET",
|
|
51164
|
+
destructiveHint: operation.method === "DELETE"
|
|
51165
|
+
}
|
|
51166
|
+
};
|
|
51167
|
+
}
|
|
51168
|
+
};
|
|
51169
|
+
function operationsFor(api) {
|
|
51170
|
+
return Object.entries(api.actions).map(([name, action]) => ({
|
|
51171
|
+
name,
|
|
51172
|
+
...action
|
|
51173
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
51174
|
+
}
|
|
51175
|
+
function getOperation(api, toolName) {
|
|
51176
|
+
const operations = operationsFor(api);
|
|
51177
|
+
const operation = operations.find((candidate) => candidate.name === toolName);
|
|
51178
|
+
if (!operation) throw new CapletsError("TOOL_NOT_FOUND", `Tool ${toolName} was not found on ${api.server}`, {
|
|
51179
|
+
server: api.server,
|
|
51180
|
+
tool: toolName,
|
|
51181
|
+
suggestions: operations.map((candidate) => candidate.name).filter((name) => name.toLocaleLowerCase().includes(toolName.toLocaleLowerCase()[0] ?? "")).slice(0, 5)
|
|
51182
|
+
});
|
|
51183
|
+
return operation;
|
|
51184
|
+
}
|
|
51185
|
+
function buildRequest$1(api, operation, args, authDir) {
|
|
51186
|
+
validateBaseUrl(api);
|
|
51187
|
+
validateAction(api, operation);
|
|
51188
|
+
const url = buildActionUrl(api.baseUrl, substitutePath$1(operation.path, args, operation), { allowEncodedSlash: true });
|
|
51189
|
+
const query = resolveMappingToRecord(operation.query, args, "query");
|
|
51190
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null) url.searchParams.append(key, serializeHttpValue$1("query", key, value));
|
|
51191
|
+
const headers = new Headers();
|
|
51192
|
+
applyAuth$1(headers, api, authDir);
|
|
51193
|
+
const resolvedHeaders = resolveMappingToRecord(operation.headers, args, "headers");
|
|
51194
|
+
for (const [key, value] of Object.entries(resolvedHeaders)) if (value !== void 0 && value !== null) {
|
|
51195
|
+
validateResolvedHeader(api, operation, key);
|
|
51196
|
+
headers.set(key, serializeHttpValue$1("header", key, value));
|
|
51197
|
+
}
|
|
51198
|
+
const bodyValue = resolveMapping(operation.jsonBody, args);
|
|
51199
|
+
if (operation.jsonBody !== void 0) {
|
|
51200
|
+
if (bodyValue === void 0) throw new CapletsError("REQUEST_INVALID", "HTTP action jsonBody must not resolve to undefined");
|
|
51201
|
+
headers.set("content-type", "application/json");
|
|
51202
|
+
return {
|
|
51203
|
+
url,
|
|
51204
|
+
headers,
|
|
51205
|
+
body: JSON.stringify(bodyValue)
|
|
51206
|
+
};
|
|
51207
|
+
}
|
|
51208
|
+
return {
|
|
51209
|
+
url,
|
|
51210
|
+
headers
|
|
51211
|
+
};
|
|
51212
|
+
}
|
|
51213
|
+
function substitutePath$1(path, args, operation) {
|
|
51214
|
+
return path.replace(/\{([^}]+)\}/g, (_match, name) => {
|
|
51215
|
+
const value = args[name];
|
|
51216
|
+
if (value === void 0 || value === null || value === "") throw new CapletsError("REQUEST_INVALID", `Missing required path parameter ${name}`, { tool: operation.name });
|
|
51217
|
+
return encodeURIComponent(serializeHttpValue$1("path", name, value));
|
|
51218
|
+
});
|
|
51219
|
+
}
|
|
51220
|
+
function resolveMapping(mapping, input) {
|
|
51221
|
+
if (typeof mapping === "string") {
|
|
51222
|
+
if (mapping === "$input") return input;
|
|
51223
|
+
if (mapping.startsWith("$input.")) return valueAtPath(input, mapping.slice(7));
|
|
51224
|
+
return mapping;
|
|
51225
|
+
}
|
|
51226
|
+
if (Array.isArray(mapping)) return mapping.map((item) => resolveMapping(item, input));
|
|
51227
|
+
if (mapping && typeof mapping === "object") return Object.fromEntries(Object.entries(mapping).map(([key, value]) => [key, resolveMapping(value, input)]));
|
|
51228
|
+
return mapping;
|
|
51229
|
+
}
|
|
51230
|
+
function resolveMappingToRecord(mapping, input, name) {
|
|
51231
|
+
if (mapping === void 0) return {};
|
|
51232
|
+
const resolved = resolveMapping(mapping, input);
|
|
51233
|
+
if (!isPlainObject$1(resolved)) throw new CapletsError("REQUEST_INVALID", `HTTP action ${name} mapping must resolve to an object`);
|
|
51234
|
+
return resolved;
|
|
51235
|
+
}
|
|
51236
|
+
function valueAtPath(input, path) {
|
|
51237
|
+
let current = input;
|
|
51238
|
+
for (const segment of path.split(".")) {
|
|
51239
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
51240
|
+
current = current[segment];
|
|
51241
|
+
}
|
|
51242
|
+
return current;
|
|
51243
|
+
}
|
|
51244
|
+
function validateAction(api, operation) {
|
|
51245
|
+
buildActionUrl(api.baseUrl, operation.path);
|
|
51246
|
+
validateConfiguredHeaders(api, operation);
|
|
51247
|
+
}
|
|
51248
|
+
function validateConfiguredHeaders(api, operation) {
|
|
51249
|
+
const configured = asRecord$1(operation.headers);
|
|
51250
|
+
for (const key of Object.keys(configured)) validateResolvedHeader(api, operation, key);
|
|
51251
|
+
}
|
|
51252
|
+
function validateResolvedHeader(api, operation, key) {
|
|
51253
|
+
const authHeaderNames = api.auth.type === "headers" ? new Set(Object.keys(api.auth.headers).map((header) => header.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
51254
|
+
const normalized = key.toLowerCase();
|
|
51255
|
+
if (FORBIDDEN_HEADERS.has(normalized) || authHeaderNames.has(normalized)) throw new CapletsError("CONFIG_INVALID", `HTTP action header ${key} is not allowed`, {
|
|
51256
|
+
server: api.server,
|
|
51257
|
+
tool: operation.name
|
|
51258
|
+
});
|
|
51259
|
+
}
|
|
51260
|
+
function serializeHttpValue$1(location, name, value) {
|
|
51261
|
+
switch (typeof value) {
|
|
51262
|
+
case "string":
|
|
51263
|
+
case "number":
|
|
51264
|
+
case "boolean": return String(value);
|
|
51265
|
+
default: throw new CapletsError("REQUEST_INVALID", `HTTP action ${location} parameter ${name} must be a string, number, or boolean`);
|
|
51266
|
+
}
|
|
51267
|
+
}
|
|
51268
|
+
function applyAuth$1(headers, api, authDir) {
|
|
51269
|
+
for (const [key, value] of Object.entries(authHeaders$1(api, authDir))) headers.set(key, value);
|
|
51270
|
+
}
|
|
51271
|
+
function authHeaders$1(api, authDir) {
|
|
51272
|
+
switch (api.auth.type) {
|
|
51273
|
+
case "none": return {};
|
|
51274
|
+
case "bearer": return { authorization: `Bearer ${api.auth.token}` };
|
|
51275
|
+
case "headers": return api.auth.headers;
|
|
51276
|
+
case "oauth2":
|
|
51277
|
+
case "oidc": return genericOAuthHeaders({
|
|
51278
|
+
server: api.server,
|
|
51279
|
+
backend: "http",
|
|
51280
|
+
baseUrl: api.baseUrl,
|
|
51281
|
+
auth: api.auth,
|
|
51282
|
+
requestTimeoutMs: api.requestTimeoutMs
|
|
51283
|
+
}, authDir);
|
|
51284
|
+
}
|
|
51285
|
+
}
|
|
51286
|
+
async function readResponse$1(response, api, elapsedMs) {
|
|
51287
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
51288
|
+
const body = parseHttpBody(contentType, await readLimitedText(response, {
|
|
51289
|
+
maxBytes: maxResponseBytes(api),
|
|
51290
|
+
errorMessage: "HTTP action response exceeded byte limit"
|
|
51291
|
+
}));
|
|
51292
|
+
return {
|
|
51293
|
+
status: response.status,
|
|
51294
|
+
statusText: response.statusText,
|
|
51295
|
+
headers: { "content-type": contentType },
|
|
51296
|
+
...body === void 0 ? {} : { body },
|
|
51297
|
+
elapsedMs
|
|
51298
|
+
};
|
|
51299
|
+
}
|
|
51300
|
+
function maxResponseBytes(api) {
|
|
51301
|
+
return api.maxResponseBytes;
|
|
51302
|
+
}
|
|
51303
|
+
function validateBaseUrl(api) {
|
|
51304
|
+
if (!isAllowedRemoteUrl(api.baseUrl)) throw new CapletsError("CONFIG_INVALID", `${api.server} HTTP API baseUrl is not allowed`);
|
|
51305
|
+
const url = new URL(api.baseUrl);
|
|
51306
|
+
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`);
|
|
51307
|
+
}
|
|
51308
|
+
function buildActionUrl(base, actionPath, options = {}) {
|
|
51309
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(actionPath) || actionPath.startsWith("//")) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot change origin");
|
|
51310
|
+
for (const rawSegment of actionPath.split("/")) {
|
|
51311
|
+
let segment;
|
|
51312
|
+
try {
|
|
51313
|
+
segment = decodeURIComponent(rawSegment);
|
|
51314
|
+
} catch {
|
|
51315
|
+
throw new CapletsError("CONFIG_INVALID", "HTTP action path contains invalid encoding");
|
|
51316
|
+
}
|
|
51317
|
+
if (segment === "." || segment === ".." || !options.allowEncodedSlash && segment.includes("/")) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot contain dot segments");
|
|
51318
|
+
}
|
|
51319
|
+
const baseUrl = new URL(base);
|
|
51320
|
+
const originalOrigin = baseUrl.origin;
|
|
51321
|
+
const basePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
51322
|
+
baseUrl.pathname = [basePath, actionPath.replace(/^\/+/, "")].filter(Boolean).join("/");
|
|
51323
|
+
if (baseUrl.origin !== originalOrigin) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot change origin");
|
|
51324
|
+
if (basePath && baseUrl.pathname !== basePath && !baseUrl.pathname.startsWith(`${basePath}/`)) throw new CapletsError("CONFIG_INVALID", "HTTP action path cannot escape baseUrl path");
|
|
51325
|
+
return baseUrl;
|
|
51326
|
+
}
|
|
51327
|
+
function asRecord$1(value) {
|
|
51328
|
+
return isPlainObject$1(value) ? value : {};
|
|
51329
|
+
}
|
|
51330
|
+
function isPlainObject$1(value) {
|
|
51331
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
51332
|
+
}
|
|
51333
|
+
//#endregion
|
|
50846
51334
|
//#region node_modules/.pnpm/@apidevtools+swagger-parser@12.1.0_openapi-types@12.1.3/node_modules/@apidevtools/swagger-parser/lib/util.js
|
|
50847
51335
|
var require_util = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
50848
51336
|
const util = __require("util");
|
|
@@ -60731,7 +61219,7 @@ var ServerRegistry = class {
|
|
|
60731
61219
|
return this.allCaplets().filter((server) => !server.disabled);
|
|
60732
61220
|
}
|
|
60733
61221
|
get(serverId) {
|
|
60734
|
-
const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId];
|
|
61222
|
+
const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId] ?? this.config.httpApis[serverId];
|
|
60735
61223
|
return server?.disabled ? void 0 : server;
|
|
60736
61224
|
}
|
|
60737
61225
|
require(serverId) {
|
|
@@ -60781,12 +61269,13 @@ var ServerRegistry = class {
|
|
|
60781
61269
|
return [
|
|
60782
61270
|
...Object.values(this.config.mcpServers),
|
|
60783
61271
|
...Object.values(this.config.openapiEndpoints),
|
|
60784
|
-
...Object.values(this.config.graphqlEndpoints)
|
|
61272
|
+
...Object.values(this.config.graphqlEndpoints),
|
|
61273
|
+
...Object.values(this.config.httpApis)
|
|
60785
61274
|
];
|
|
60786
61275
|
}
|
|
60787
61276
|
};
|
|
60788
61277
|
function capabilityDescription(server) {
|
|
60789
|
-
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : "GraphQL endpoint";
|
|
61278
|
+
const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
|
|
60790
61279
|
const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
|
|
60791
61280
|
const hint = [
|
|
60792
61281
|
`Use this Caplet to inspect and call tools from its ${backendName} backend.`,
|
|
@@ -60818,6 +61307,12 @@ function backendDetail(server) {
|
|
|
60818
61307
|
source: graphQlSource(server),
|
|
60819
61308
|
configuredOperations: Boolean(server.operations && Object.keys(server.operations).length > 0)
|
|
60820
61309
|
};
|
|
61310
|
+
if (server.backend === "http") return {
|
|
61311
|
+
type: "http",
|
|
61312
|
+
disabled: server.disabled,
|
|
61313
|
+
requestTimeoutMs: server.requestTimeoutMs,
|
|
61314
|
+
configuredActions: Object.keys(server.actions).length
|
|
61315
|
+
};
|
|
60821
61316
|
return {
|
|
60822
61317
|
type: "mcp",
|
|
60823
61318
|
transport: server.transport,
|
|
@@ -60854,16 +61349,16 @@ const generatedToolInputSchema = object$1({
|
|
|
60854
61349
|
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
61350
|
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
61351
|
}).strict();
|
|
60857
|
-
async function handleServerTool(server, request, registry, downstream, openapi, graphql) {
|
|
61352
|
+
async function handleServerTool(server, request, registry, downstream, openapi, graphql, http) {
|
|
60858
61353
|
const parsed = validateOperationRequest(request, registry.config.options.maxSearchLimit);
|
|
60859
61354
|
switch (parsed.operation) {
|
|
60860
61355
|
case "get_caplet": return jsonResult(registry.detail(server));
|
|
60861
|
-
case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql).check(server));
|
|
61356
|
+
case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql, http).check(server));
|
|
60862
61357
|
case "check_mcp_server":
|
|
60863
61358
|
if (server.backend !== "mcp") throw new CapletsError("REQUEST_INVALID", "check_mcp_server is only valid for MCP-backed Caplets; use check_backend");
|
|
60864
61359
|
return jsonResult(await downstream.checkServer(server));
|
|
60865
61360
|
case "list_tools": {
|
|
60866
|
-
const backend = backendFor(server, downstream, openapi, graphql);
|
|
61361
|
+
const backend = backendFor(server, downstream, openapi, graphql, http);
|
|
60867
61362
|
const tools = await backend.listTools(server);
|
|
60868
61363
|
return jsonResult({
|
|
60869
61364
|
server: server.server,
|
|
@@ -60871,7 +61366,7 @@ async function handleServerTool(server, request, registry, downstream, openapi,
|
|
|
60871
61366
|
});
|
|
60872
61367
|
}
|
|
60873
61368
|
case "search_tools": {
|
|
60874
|
-
const backend = backendFor(server, downstream, openapi, graphql);
|
|
61369
|
+
const backend = backendFor(server, downstream, openapi, graphql, http);
|
|
60875
61370
|
const tools = await backend.listTools(server);
|
|
60876
61371
|
const limit = parsed.limit ?? registry.config.options.defaultSearchLimit;
|
|
60877
61372
|
return jsonResult({
|
|
@@ -60881,13 +61376,13 @@ async function handleServerTool(server, request, registry, downstream, openapi,
|
|
|
60881
61376
|
});
|
|
60882
61377
|
}
|
|
60883
61378
|
case "get_tool": {
|
|
60884
|
-
const tool = await backendFor(server, downstream, openapi, graphql).getTool(server, parsed.tool);
|
|
61379
|
+
const tool = await backendFor(server, downstream, openapi, graphql, http).getTool(server, parsed.tool);
|
|
60885
61380
|
return jsonResult({
|
|
60886
61381
|
server: server.server,
|
|
60887
61382
|
tool
|
|
60888
61383
|
});
|
|
60889
61384
|
}
|
|
60890
|
-
case "call_tool": return backendFor(server, downstream, openapi, graphql).callTool(server, parsed.tool, parsed.arguments);
|
|
61385
|
+
case "call_tool": return backendFor(server, downstream, openapi, graphql, http).callTool(server, parsed.tool, parsed.arguments);
|
|
60891
61386
|
}
|
|
60892
61387
|
}
|
|
60893
61388
|
function validateOperationRequest(request, maxSearchLimit) {
|
|
@@ -60950,7 +61445,7 @@ function jsonResult(value) {
|
|
|
60950
61445
|
function isPlainObject(value) {
|
|
60951
61446
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60952
61447
|
}
|
|
60953
|
-
function backendFor(server, downstream, openapi, graphql) {
|
|
61448
|
+
function backendFor(server, downstream, openapi, graphql, http) {
|
|
60954
61449
|
if (server.backend === "mcp") return {
|
|
60955
61450
|
check: (...args) => downstream.checkServer(...args),
|
|
60956
61451
|
listTools: (...args) => downstream.listTools(...args),
|
|
@@ -60970,6 +61465,17 @@ function backendFor(server, downstream, openapi, graphql) {
|
|
|
60970
61465
|
search: (...args) => graphql.search(...args)
|
|
60971
61466
|
};
|
|
60972
61467
|
}
|
|
61468
|
+
if (server.backend === "http") {
|
|
61469
|
+
if (!http) throw new CapletsError("INTERNAL_ERROR", "HTTP action manager is not configured");
|
|
61470
|
+
return {
|
|
61471
|
+
check: (...args) => http.checkApi(...args),
|
|
61472
|
+
listTools: (...args) => http.listTools(...args),
|
|
61473
|
+
getTool: (...args) => http.getTool(...args),
|
|
61474
|
+
callTool: (...args) => http.callTool(...args),
|
|
61475
|
+
compact: (...args) => http.compact(...args),
|
|
61476
|
+
search: (...args) => http.search(...args)
|
|
61477
|
+
};
|
|
61478
|
+
}
|
|
60973
61479
|
if (!openapi) throw new CapletsError("INTERNAL_ERROR", "OpenAPI manager is not configured");
|
|
60974
61480
|
return {
|
|
60975
61481
|
check: (...args) => openapi.checkEndpoint(...args),
|
|
@@ -60988,6 +61494,7 @@ var CapletsRuntime = class {
|
|
|
60988
61494
|
downstream;
|
|
60989
61495
|
openapi;
|
|
60990
61496
|
graphql;
|
|
61497
|
+
http;
|
|
60991
61498
|
tools = /* @__PURE__ */ new Map();
|
|
60992
61499
|
paths;
|
|
60993
61500
|
watchDebounceMs;
|
|
@@ -61008,6 +61515,7 @@ var CapletsRuntime = class {
|
|
|
61008
61515
|
this.downstream = new DownstreamManager(this.registry, selectAuthOptions(options.authDir));
|
|
61009
61516
|
this.openapi = new OpenApiManager(this.registry, selectAuthOptions(options.authDir));
|
|
61010
61517
|
this.graphql = new GraphQLManager(this.registry, selectAuthOptions(options.authDir));
|
|
61518
|
+
this.http = new HttpActionManager(this.registry, selectAuthOptions(options.authDir));
|
|
61011
61519
|
this.server = options.server ?? new McpServer({
|
|
61012
61520
|
name: "caplets",
|
|
61013
61521
|
version
|
|
@@ -61083,6 +61591,7 @@ var CapletsRuntime = class {
|
|
|
61083
61591
|
this.downstream.updateRegistry(nextRegistry);
|
|
61084
61592
|
this.openapi.updateRegistry(nextRegistry);
|
|
61085
61593
|
this.graphql.updateRegistry(nextRegistry);
|
|
61594
|
+
this.http.updateRegistry(nextRegistry);
|
|
61086
61595
|
let invalidated = true;
|
|
61087
61596
|
try {
|
|
61088
61597
|
await this.invalidateChangedBackends(previousConfig, nextConfig);
|
|
@@ -61141,7 +61650,7 @@ var CapletsRuntime = class {
|
|
|
61141
61650
|
}
|
|
61142
61651
|
async handleTool(serverId, request) {
|
|
61143
61652
|
try {
|
|
61144
|
-
return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql);
|
|
61653
|
+
return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql, this.http);
|
|
61145
61654
|
} catch (error) {
|
|
61146
61655
|
return errorResult(error);
|
|
61147
61656
|
}
|
|
@@ -61157,6 +61666,7 @@ var CapletsRuntime = class {
|
|
|
61157
61666
|
if (before?.backend === "mcp") await this.downstream.closeServer(serverId);
|
|
61158
61667
|
if (before?.backend === "openapi" || after?.backend === "openapi" || !after) this.openapi.invalidate(serverId);
|
|
61159
61668
|
if (before?.backend === "graphql" || after?.backend === "graphql" || !after) this.graphql.invalidate(serverId);
|
|
61669
|
+
if (before?.backend === "http" || after?.backend === "http" || !after) this.http.invalidate(serverId);
|
|
61160
61670
|
}
|
|
61161
61671
|
}
|
|
61162
61672
|
resetWatchers() {
|
|
@@ -61248,14 +61758,15 @@ function allCaplets(config) {
|
|
|
61248
61758
|
return [
|
|
61249
61759
|
...Object.values(config.mcpServers),
|
|
61250
61760
|
...Object.values(config.openapiEndpoints),
|
|
61251
|
-
...Object.values(config.graphqlEndpoints)
|
|
61761
|
+
...Object.values(config.graphqlEndpoints),
|
|
61762
|
+
...Object.values(config.httpApis)
|
|
61252
61763
|
];
|
|
61253
61764
|
}
|
|
61254
61765
|
function nextEnabledServers(config) {
|
|
61255
61766
|
return allCaplets(config).filter((server) => !server.disabled);
|
|
61256
61767
|
}
|
|
61257
61768
|
function capletById(config, serverId) {
|
|
61258
|
-
return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId];
|
|
61769
|
+
return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId] ?? config.httpApis[serverId];
|
|
61259
61770
|
}
|
|
61260
61771
|
function serializeCaplet(caplet) {
|
|
61261
61772
|
return JSON.stringify(caplet ?? null);
|