@sidecar-ai/cli 0.1.0-alpha.1 → 0.1.0-alpha.4
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/dist/index.js +1112 -153
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/cli/src/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { mkdir as
|
|
4
|
+
import { existsSync as existsSync6, realpathSync } from "fs";
|
|
5
|
+
import { mkdir as mkdir10, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
|
|
6
6
|
import { createServer as createServer2 } from "http";
|
|
7
|
-
import
|
|
7
|
+
import path19 from "path";
|
|
8
8
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
10
10
|
import { cwd, exit, stdin as stdin2, stdout as stdout2 } from "process";
|
|
@@ -14,7 +14,7 @@ import { tsImport } from "tsx/esm/api";
|
|
|
14
14
|
var authBrand = /* @__PURE__ */ Symbol.for("sidecar.auth");
|
|
15
15
|
function isSidecarAuth(value) {
|
|
16
16
|
return Boolean(
|
|
17
|
-
value && typeof value === "object" && value[authBrand]
|
|
17
|
+
value && typeof value === "object" && (value[authBrand] || value.kind === "sidecar.auth")
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -26,13 +26,19 @@ var resourceBrand = /* @__PURE__ */ Symbol.for("sidecar.resource");
|
|
|
26
26
|
var resourceResultBrand = /* @__PURE__ */ Symbol.for("sidecar.resourceResult");
|
|
27
27
|
var promptBrand = /* @__PURE__ */ Symbol.for("sidecar.prompt");
|
|
28
28
|
function isSidecarTool(value) {
|
|
29
|
-
return Boolean(
|
|
29
|
+
return Boolean(
|
|
30
|
+
value && typeof value === "object" && (value[toolBrand] || value.kind === "sidecar.tool")
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
function isSidecarResource(value) {
|
|
32
|
-
return Boolean(
|
|
34
|
+
return Boolean(
|
|
35
|
+
value && typeof value === "object" && (value[resourceBrand] || value.kind === "sidecar.resource")
|
|
36
|
+
);
|
|
33
37
|
}
|
|
34
38
|
function isSidecarPrompt(value) {
|
|
35
|
-
return Boolean(
|
|
39
|
+
return Boolean(
|
|
40
|
+
value && typeof value === "object" && (value[promptBrand] || value.kind === "sidecar.prompt")
|
|
41
|
+
);
|
|
36
42
|
}
|
|
37
43
|
var toolResult = Object.assign(
|
|
38
44
|
createToolResult,
|
|
@@ -755,12 +761,19 @@ function typeToJsonSchemaInner(type) {
|
|
|
755
761
|
return { anyOf: parts.map((part) => typeToJsonSchema(part)) };
|
|
756
762
|
}
|
|
757
763
|
const properties = type.getProperties();
|
|
764
|
+
const indexType = type.getStringIndexType() ?? type.getNumberIndexType();
|
|
758
765
|
if (properties.length > 0) {
|
|
759
|
-
return objectTypeToSchema(properties);
|
|
766
|
+
return objectTypeToSchema(properties, indexType);
|
|
767
|
+
}
|
|
768
|
+
if (indexType) {
|
|
769
|
+
return {
|
|
770
|
+
type: "object",
|
|
771
|
+
additionalProperties: indexTypeToAdditionalProperties(indexType)
|
|
772
|
+
};
|
|
760
773
|
}
|
|
761
774
|
return {};
|
|
762
775
|
}
|
|
763
|
-
function objectTypeToSchema(properties) {
|
|
776
|
+
function objectTypeToSchema(properties, indexType) {
|
|
764
777
|
const schemaProperties = {};
|
|
765
778
|
const required = [];
|
|
766
779
|
for (const property of properties) {
|
|
@@ -784,9 +797,16 @@ function objectTypeToSchema(properties) {
|
|
|
784
797
|
type: "object",
|
|
785
798
|
properties: schemaProperties,
|
|
786
799
|
required,
|
|
787
|
-
additionalProperties: false
|
|
800
|
+
additionalProperties: indexType ? indexTypeToAdditionalProperties(indexType) : false
|
|
788
801
|
};
|
|
789
802
|
}
|
|
803
|
+
function indexTypeToAdditionalProperties(type) {
|
|
804
|
+
if (type.isAny() || type.isUnknown()) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
const schema = typeToJsonSchema(type);
|
|
808
|
+
return Object.keys(schema).length ? schema : true;
|
|
809
|
+
}
|
|
790
810
|
function literalOrPrimitive(type, primitive) {
|
|
791
811
|
if (isLiteralType(type)) {
|
|
792
812
|
return { const: literalValue(type) };
|
|
@@ -855,6 +875,7 @@ import {
|
|
|
855
875
|
Node as Node3,
|
|
856
876
|
SyntaxKind
|
|
857
877
|
} from "ts-morph";
|
|
878
|
+
var CLAUDE_FONT_RESOURCE_DOMAIN = "https://assets.claude.ai";
|
|
858
879
|
async function buildWidgets(rootDir, outDir, tools) {
|
|
859
880
|
const widgets = tools.filter(
|
|
860
881
|
(entry) => Boolean(entry.widget)
|
|
@@ -1038,7 +1059,7 @@ function widgetMeta(resourceUri, options = {}, target = "mcp") {
|
|
|
1038
1059
|
function widgetResourceMeta(options = {}, target = "mcp") {
|
|
1039
1060
|
const csp = stripUndefined3({
|
|
1040
1061
|
connectDomains: options.csp?.connectDomains ? [...options.csp.connectDomains] : [],
|
|
1041
|
-
resourceDomains: options
|
|
1062
|
+
resourceDomains: widgetResourceDomains(options, target),
|
|
1042
1063
|
frameDomains: options.csp?.frameDomains ? [...options.csp.frameDomains] : void 0,
|
|
1043
1064
|
baseUriDomains: options.csp?.baseUriDomains ? [...options.csp.baseUriDomains] : void 0
|
|
1044
1065
|
});
|
|
@@ -1051,6 +1072,13 @@ function widgetResourceMeta(options = {}, target = "mcp") {
|
|
|
1051
1072
|
});
|
|
1052
1073
|
return Object.keys(ui).length ? { ui } : void 0;
|
|
1053
1074
|
}
|
|
1075
|
+
function widgetResourceDomains(options, target) {
|
|
1076
|
+
const domains = [...options.csp?.resourceDomains ?? []];
|
|
1077
|
+
if (target === "claude" && !domains.includes(CLAUDE_FONT_RESOURCE_DOMAIN)) {
|
|
1078
|
+
domains.push(CLAUDE_FONT_RESOURCE_DOMAIN);
|
|
1079
|
+
}
|
|
1080
|
+
return domains;
|
|
1081
|
+
}
|
|
1054
1082
|
function findWidgetDefinition(sourceFile) {
|
|
1055
1083
|
const call = resolveDefaultExportCall(sourceFile, "widget");
|
|
1056
1084
|
if (!call) {
|
|
@@ -1636,8 +1664,8 @@ function readStringArrayProperty2(definition, propertyName) {
|
|
|
1636
1664
|
}
|
|
1637
1665
|
|
|
1638
1666
|
// packages/compiler/src/build.ts
|
|
1639
|
-
import { mkdir as
|
|
1640
|
-
import
|
|
1667
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
1668
|
+
import path18 from "path";
|
|
1641
1669
|
|
|
1642
1670
|
// packages/compiler/src/config.ts
|
|
1643
1671
|
import path5 from "path";
|
|
@@ -2054,10 +2082,6 @@ function uniqueMethodNames(names) {
|
|
|
2054
2082
|
});
|
|
2055
2083
|
}
|
|
2056
2084
|
|
|
2057
|
-
// packages/compiler/src/plugins.ts
|
|
2058
|
-
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
2059
|
-
import path14 from "path";
|
|
2060
|
-
|
|
2061
2085
|
// packages/compiler/src/identity.ts
|
|
2062
2086
|
import { readFile as readFile3 } from "fs/promises";
|
|
2063
2087
|
import path8 from "path";
|
|
@@ -2098,6 +2122,10 @@ function readConfigString(configText, key) {
|
|
|
2098
2122
|
return match?.[1];
|
|
2099
2123
|
}
|
|
2100
2124
|
|
|
2125
|
+
// packages/compiler/src/plugins.ts
|
|
2126
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
2127
|
+
import path14 from "path";
|
|
2128
|
+
|
|
2101
2129
|
// packages/compiler/src/plugin-components/agents.ts
|
|
2102
2130
|
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
2103
2131
|
import path9 from "path";
|
|
@@ -2542,7 +2570,7 @@ This package was generated by Sidecar.
|
|
|
2542
2570
|
|
|
2543
2571
|
Set \`SIDECAR_MCP_URL\` to the hosted MCP endpoint for this app.
|
|
2544
2572
|
|
|
2545
|
-
For local testing, run \`sidecar dev --tunnel\` and use the
|
|
2573
|
+
For local testing, run \`sidecar dev --tunnel\` and use the validated HTTPS MCP URL it prints. Temporary quick tunnels are public and best-effort; use a configured tunnel/domain or deployed preview for repeatable testing.
|
|
2546
2574
|
|
|
2547
2575
|
Claude/Cowork plugin-specific components such as skills, slash commands, agents, hooks, output styles, themes, monitors, assets, and bin scripts will be emitted here when the source project defines them.
|
|
2548
2576
|
`
|
|
@@ -2953,15 +2981,319 @@ function readStringArrayProperty4(definition, propertyName) {
|
|
|
2953
2981
|
return initializer.getElements().filter(Node8.isStringLiteral).map((element) => element.getLiteralText());
|
|
2954
2982
|
}
|
|
2955
2983
|
|
|
2984
|
+
// packages/compiler/src/server-output.ts
|
|
2985
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2986
|
+
import { mkdir as mkdir8, rm, writeFile as writeFile8 } from "fs/promises";
|
|
2987
|
+
import path17 from "path";
|
|
2988
|
+
import { build as esbuild2 } from "esbuild";
|
|
2989
|
+
var SERVER_ENTRYPOINT = "server/index.js";
|
|
2990
|
+
var VERCEL_ENTRYPOINT = "api/sidecar.js";
|
|
2991
|
+
async function buildServerOutput(rootDir, outDir, manifest, identity, host = "node") {
|
|
2992
|
+
const cacheDir = path17.join(rootDir, ".sidecar", "cache", "server");
|
|
2993
|
+
await mkdir8(cacheDir, { recursive: true });
|
|
2994
|
+
const entryFile = path17.join(cacheDir, "index.ts");
|
|
2995
|
+
await writeFile8(entryFile, renderServerEntry(rootDir, entryFile, manifest, identity));
|
|
2996
|
+
const serverFile = path17.join(outDir, SERVER_ENTRYPOINT);
|
|
2997
|
+
await mkdir8(path17.dirname(serverFile), { recursive: true });
|
|
2998
|
+
await esbuild2({
|
|
2999
|
+
absWorkingDir: rootDir,
|
|
3000
|
+
alias: sidecarBundleAliases(rootDir),
|
|
3001
|
+
banner: {
|
|
3002
|
+
js: `import { createRequire as __sidecarCreateRequire } from "node:module";
|
|
3003
|
+
const require = __sidecarCreateRequire(import.meta.url);`
|
|
3004
|
+
},
|
|
3005
|
+
bundle: true,
|
|
3006
|
+
entryPoints: [entryFile],
|
|
3007
|
+
format: "esm",
|
|
3008
|
+
legalComments: "none",
|
|
3009
|
+
minify: false,
|
|
3010
|
+
nodePaths: [
|
|
3011
|
+
path17.join(rootDir, "node_modules"),
|
|
3012
|
+
path17.join(process.cwd(), "node_modules")
|
|
3013
|
+
],
|
|
3014
|
+
outfile: serverFile,
|
|
3015
|
+
platform: "node",
|
|
3016
|
+
sourcemap: false,
|
|
3017
|
+
target: "node20"
|
|
3018
|
+
});
|
|
3019
|
+
await writeFile8(path17.join(outDir, "package.json"), renderServerPackage(identity));
|
|
3020
|
+
if (host === "vercel") {
|
|
3021
|
+
await mkdir8(path17.join(outDir, "api"), { recursive: true });
|
|
3022
|
+
await writeFile8(path17.join(outDir, VERCEL_ENTRYPOINT), renderVercelEntrypoint());
|
|
3023
|
+
await writeFile8(path17.join(outDir, "vercel.json"), renderVercelConfig());
|
|
3024
|
+
} else {
|
|
3025
|
+
await rm(path17.join(outDir, "api"), { recursive: true, force: true });
|
|
3026
|
+
await rm(path17.join(outDir, "vercel.json"), { force: true });
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function renderServerEntry(rootDir, entryFile, manifest, identity) {
|
|
3030
|
+
const entryDir = path17.dirname(entryFile);
|
|
3031
|
+
const tools = manifest.tools;
|
|
3032
|
+
const resources = manifest.resources;
|
|
3033
|
+
const prompts = manifest.prompts;
|
|
3034
|
+
const imports = [
|
|
3035
|
+
`import { readFileSync, realpathSync } from "node:fs";`,
|
|
3036
|
+
`import { createServer } from "node:http";`,
|
|
3037
|
+
`import { pathToFileURL } from "node:url";`,
|
|
3038
|
+
`import { createSidecarHttpHandler } from "@sidecar-ai/server";`,
|
|
3039
|
+
`import { isSidecarAuth } from "@sidecar-ai/auth";`,
|
|
3040
|
+
`import { isSidecarPrompt, isSidecarResource, isSidecarTool } from "@sidecar-ai/core";`,
|
|
3041
|
+
`import { isSidecarProxy } from "@sidecar-ai/server/proxy";`,
|
|
3042
|
+
...tools.map(
|
|
3043
|
+
(entry, index) => `import tool${index} from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, entry.sourceFile)))};`
|
|
3044
|
+
),
|
|
3045
|
+
...resources.map(
|
|
3046
|
+
(entry, index) => `import resource${index} from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, entry.sourceFile)))};`
|
|
3047
|
+
),
|
|
3048
|
+
...prompts.map(
|
|
3049
|
+
(entry, index) => `import prompt${index} from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, entry.sourceFile)))};`
|
|
3050
|
+
),
|
|
3051
|
+
existsSync5(path17.join(rootDir, "auth.ts")) ? `import authExport from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, "auth.ts")))};` : `const authExport = undefined;`,
|
|
3052
|
+
existsSync5(path17.join(rootDir, "proxy.ts")) ? `import proxyExport from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, "proxy.ts")))};` : `const proxyExport = undefined;`,
|
|
3053
|
+
existsSync5(path17.join(rootDir, "sidecar.config.ts")) ? `import configExport from ${JSON.stringify(toImportSpecifier(entryDir, path17.join(rootDir, "sidecar.config.ts")))};` : `const configExport = undefined;`
|
|
3054
|
+
].join("\n");
|
|
3055
|
+
return `${imports}
|
|
3056
|
+
|
|
3057
|
+
const manifest = ${JSON.stringify(manifest, null, 2)};
|
|
3058
|
+
const identity = ${JSON.stringify(identity, null, 2)};
|
|
3059
|
+
|
|
3060
|
+
const loadedAuth = authExport === undefined ? undefined : assertAuth(authExport);
|
|
3061
|
+
const auth = loadedAuth && process.env.SIDECAR_MCP_URL
|
|
3062
|
+
? loadedAuth.withResource(process.env.SIDECAR_MCP_URL)
|
|
3063
|
+
: loadedAuth;
|
|
3064
|
+
const proxy = proxyExport === undefined ? undefined : assertProxy(proxyExport);
|
|
3065
|
+
const runtimeConfig = configExport && typeof configExport === "object" ? configExport : undefined;
|
|
3066
|
+
|
|
3067
|
+
if (auth && !process.env.SIDECAR_MCP_URL) {
|
|
3068
|
+
console.warn("Sidecar auth is enabled. Set SIDECAR_MCP_URL to the public https://.../mcp URL before hosting.");
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
export const handler = createSidecarHttpHandler({
|
|
3072
|
+
name: identity.name,
|
|
3073
|
+
version: identity.version,
|
|
3074
|
+
path: process.env.SIDECAR_MCP_PATH ?? "/mcp",
|
|
3075
|
+
publicUrl: process.env.SIDECAR_PUBLIC_URL ?? process.env.SIDECAR_MCP_URL,
|
|
3076
|
+
auth,
|
|
3077
|
+
proxy,
|
|
3078
|
+
tools: [
|
|
3079
|
+
${tools.map((entry, index) => ` loadTool(${JSON.stringify(entry.sourceFile)}, tool${index}, manifest.tools[${index}].descriptor),`).join("\n")}
|
|
3080
|
+
],
|
|
3081
|
+
resources: [
|
|
3082
|
+
${renderWidgetResources(manifest)}
|
|
3083
|
+
${resources.map((entry, index) => ` loadResource(${JSON.stringify(entry.sourceFile)}, resource${index}, manifest.resources[${index}]),`).join("\n")}
|
|
3084
|
+
],
|
|
3085
|
+
resourceTemplates: manifest.resourceTemplates.map((entry) => ({ descriptor: entry.descriptor })),
|
|
3086
|
+
prompts: [
|
|
3087
|
+
${prompts.map((entry, index) => ` loadPrompt(${JSON.stringify(entry.sourceFile)}, prompt${index}, manifest.prompts[${index}].descriptor),`).join("\n")}
|
|
3088
|
+
],
|
|
3089
|
+
capabilities: {
|
|
3090
|
+
tools: runtimeConfig?.tools ?? manifest.config.tools,
|
|
3091
|
+
resources: runtimeConfig?.resources ?? manifest.config.resources,
|
|
3092
|
+
prompts: runtimeConfig?.prompts ?? manifest.config.prompts,
|
|
3093
|
+
},
|
|
3094
|
+
pagination: runtimeConfig?.pagination ?? {
|
|
3095
|
+
pageSize: manifest.config.pagination.pageSize,
|
|
3096
|
+
},
|
|
3097
|
+
});
|
|
3098
|
+
|
|
3099
|
+
export default handler;
|
|
3100
|
+
|
|
3101
|
+
export const server = createServer(handler);
|
|
3102
|
+
|
|
3103
|
+
if (isDirectRun()) {
|
|
3104
|
+
const port = readPort();
|
|
3105
|
+
const host = process.env.SIDECAR_HOST ?? process.env.HOST ?? "0.0.0.0";
|
|
3106
|
+
server.listen(port, host, () => {
|
|
3107
|
+
const localHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
3108
|
+
console.log(\`MCP running on Streamable HTTP at http://\${localHost}:\${port}\${process.env.SIDECAR_MCP_PATH ?? "/mcp"}\`);
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
process.on("SIGTERM", () => shutdown());
|
|
3112
|
+
process.on("SIGINT", () => shutdown());
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function loadTool(sourceFile, value, descriptor) {
|
|
3116
|
+
const tool = unwrapRuntimeDefault(value);
|
|
3117
|
+
if (!isSidecarTool(tool)) {
|
|
3118
|
+
throw new Error(\`\${sourceFile} did not default-export a Sidecar tool.\`);
|
|
3119
|
+
}
|
|
3120
|
+
return { tool, descriptor };
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function loadResource(sourceFile, value, entry) {
|
|
3124
|
+
const resource = unwrapRuntimeDefault(value);
|
|
3125
|
+
if (!isSidecarResource(resource)) {
|
|
3126
|
+
throw new Error(\`\${sourceFile} did not default-export a Sidecar resource.\`);
|
|
3127
|
+
}
|
|
3128
|
+
return {
|
|
3129
|
+
uri: entry.uri,
|
|
3130
|
+
descriptor: entry.descriptor,
|
|
3131
|
+
resource,
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
function loadPrompt(sourceFile, value, descriptor) {
|
|
3136
|
+
const prompt = unwrapRuntimeDefault(value);
|
|
3137
|
+
if (!isSidecarPrompt(prompt)) {
|
|
3138
|
+
throw new Error(\`\${sourceFile} did not default-export a Sidecar prompt.\`);
|
|
3139
|
+
}
|
|
3140
|
+
return { prompt, descriptor };
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function assertAuth(value) {
|
|
3144
|
+
const authValue = unwrapRuntimeDefault(value);
|
|
3145
|
+
if (!isSidecarAuth(authValue)) {
|
|
3146
|
+
throw new Error("auth.ts must default-export auth({ ... }) from sidecar-ai.");
|
|
3147
|
+
}
|
|
3148
|
+
return authValue;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
function assertProxy(value) {
|
|
3152
|
+
const proxyValue = unwrapRuntimeDefault(value);
|
|
3153
|
+
if (!isSidecarProxy(proxyValue)) {
|
|
3154
|
+
throw new Error("proxy.ts must default-export proxy({ ... }) from @sidecar-ai/server/proxy.");
|
|
3155
|
+
}
|
|
3156
|
+
return proxyValue;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function unwrapRuntimeDefault(value) {
|
|
3160
|
+
if (
|
|
3161
|
+
value &&
|
|
3162
|
+
typeof value === "object" &&
|
|
3163
|
+
"default" in value &&
|
|
3164
|
+
Object.keys(value).length === 1
|
|
3165
|
+
) {
|
|
3166
|
+
return unwrapRuntimeDefault(value.default);
|
|
3167
|
+
}
|
|
3168
|
+
return value;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function readWidget(outputFile) {
|
|
3172
|
+
return readFileSync(new URL(\`../\${outputFile}\`, import.meta.url), "utf8");
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
function readPort() {
|
|
3176
|
+
const raw = process.env.PORT ?? process.env.SIDECAR_PORT ?? "3001";
|
|
3177
|
+
const port = Number(raw);
|
|
3178
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
3179
|
+
throw new Error(\`Invalid PORT/SIDECAR_PORT value: \${raw}\`);
|
|
3180
|
+
}
|
|
3181
|
+
return port;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
function isDirectRun() {
|
|
3185
|
+
const entry = process.argv[1];
|
|
3186
|
+
if (!entry) {
|
|
3187
|
+
return false;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function shutdown() {
|
|
3194
|
+
server.close(() => process.exit(0));
|
|
3195
|
+
}
|
|
3196
|
+
`;
|
|
3197
|
+
}
|
|
3198
|
+
function renderWidgetResources(manifest) {
|
|
3199
|
+
return manifest.tools.filter((entry) => entry.widget?.outputFile).map((entry) => ` {
|
|
3200
|
+
uri: ${JSON.stringify(entry.widget?.resourceUri)},
|
|
3201
|
+
name: ${JSON.stringify(entry.name)},
|
|
3202
|
+
description: ${JSON.stringify(entry.widget?.options?.description)},
|
|
3203
|
+
mimeType: "text/html;profile=mcp-app",
|
|
3204
|
+
text: readWidget(${JSON.stringify(entry.widget?.outputFile)}),
|
|
3205
|
+
_meta: ${JSON.stringify(entry.widget?.resourceMeta ?? void 0)},
|
|
3206
|
+
},`).join("\n");
|
|
3207
|
+
}
|
|
3208
|
+
function renderServerPackage(identity) {
|
|
3209
|
+
return `${JSON.stringify({
|
|
3210
|
+
name: `${identity.slug}-sidecar-server`,
|
|
3211
|
+
version: identity.version,
|
|
3212
|
+
private: true,
|
|
3213
|
+
type: "module",
|
|
3214
|
+
scripts: {
|
|
3215
|
+
start: "node server/index.js"
|
|
3216
|
+
},
|
|
3217
|
+
engines: {
|
|
3218
|
+
node: ">=20"
|
|
3219
|
+
}
|
|
3220
|
+
}, null, 2)}
|
|
3221
|
+
`;
|
|
3222
|
+
}
|
|
3223
|
+
function renderVercelEntrypoint() {
|
|
3224
|
+
return `export { default } from "../server/index.js";
|
|
3225
|
+
`;
|
|
3226
|
+
}
|
|
3227
|
+
function renderVercelConfig() {
|
|
3228
|
+
return `${JSON.stringify({
|
|
3229
|
+
rewrites: [
|
|
3230
|
+
{
|
|
3231
|
+
source: "/(.*)",
|
|
3232
|
+
destination: "/api/sidecar"
|
|
3233
|
+
}
|
|
3234
|
+
],
|
|
3235
|
+
functions: {
|
|
3236
|
+
"api/sidecar.js": {
|
|
3237
|
+
includeFiles: "public/**",
|
|
3238
|
+
maxDuration: 300
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}, null, 2)}
|
|
3242
|
+
`;
|
|
3243
|
+
}
|
|
3244
|
+
function sidecarBundleAliases(rootDir) {
|
|
3245
|
+
const repoRoot = findSidecarRepoRoot2(rootDir) ?? findSidecarRepoRoot2(process.cwd());
|
|
3246
|
+
if (!repoRoot) {
|
|
3247
|
+
return void 0;
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
"sidecar-ai": path17.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
|
|
3251
|
+
"@sidecar-ai/auth": path17.join(repoRoot, "packages", "auth", "src", "index.ts"),
|
|
3252
|
+
"@sidecar-ai/core": path17.join(repoRoot, "packages", "core", "src", "index.ts"),
|
|
3253
|
+
"@sidecar-ai/server": path17.join(repoRoot, "packages", "server", "src", "index.ts"),
|
|
3254
|
+
"@sidecar-ai/server/proxy": path17.join(repoRoot, "packages", "server", "src", "proxy.ts"),
|
|
3255
|
+
"@sidecar-ai/client": path17.join(repoRoot, "packages", "client", "src", "index.ts"),
|
|
3256
|
+
"@sidecar-ai/react": path17.join(repoRoot, "packages", "react", "src", "index.ts"),
|
|
3257
|
+
"@sidecar-ai/native": path17.join(repoRoot, "packages", "native", "src", "index.ts"),
|
|
3258
|
+
"@sidecar-ai/native/components": path17.join(repoRoot, "packages", "native", "src", "components", "index.tsx"),
|
|
3259
|
+
"@sidecar-ai/openai": path17.join(repoRoot, "packages", "openai", "src", "index.ts"),
|
|
3260
|
+
"@sidecar-ai/openai/components": path17.join(repoRoot, "packages", "openai", "src", "components.tsx"),
|
|
3261
|
+
"@sidecar-ai/openai/official": path17.join(repoRoot, "packages", "openai", "src", "official.ts"),
|
|
3262
|
+
"@sidecar-ai/anthropic": path17.join(repoRoot, "packages", "anthropic", "src", "index.ts"),
|
|
3263
|
+
"@sidecar-ai/anthropic/agent": path17.join(repoRoot, "packages", "anthropic", "src", "agent.ts"),
|
|
3264
|
+
"@sidecar-ai/anthropic/command": path17.join(repoRoot, "packages", "anthropic", "src", "command.ts"),
|
|
3265
|
+
"@sidecar-ai/anthropic/components": path17.join(repoRoot, "packages", "anthropic", "src", "components.tsx"),
|
|
3266
|
+
"@sidecar-ai/anthropic/hooks": path17.join(repoRoot, "packages", "anthropic", "src", "hooks.ts"),
|
|
3267
|
+
"@sidecar-ai/anthropic/mcp": path17.join(repoRoot, "packages", "anthropic", "src", "mcp.ts"),
|
|
3268
|
+
"@sidecar-ai/anthropic/plugin": path17.join(repoRoot, "packages", "anthropic", "src", "plugin.ts"),
|
|
3269
|
+
"@sidecar-ai/anthropic/skill": path17.join(repoRoot, "packages", "anthropic", "src", "skill.ts")
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function findSidecarRepoRoot2(startDir) {
|
|
3273
|
+
let current = path17.resolve(startDir);
|
|
3274
|
+
while (true) {
|
|
3275
|
+
if (existsSync5(path17.join(current, "packages", "core", "src", "index.ts"))) {
|
|
3276
|
+
return current;
|
|
3277
|
+
}
|
|
3278
|
+
const parent = path17.dirname(current);
|
|
3279
|
+
if (parent === current) {
|
|
3280
|
+
return void 0;
|
|
3281
|
+
}
|
|
3282
|
+
current = parent;
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
|
|
2956
3286
|
// packages/compiler/src/build.ts
|
|
2957
3287
|
async function buildProject(options) {
|
|
2958
|
-
const rootDir =
|
|
3288
|
+
const rootDir = path18.resolve(options.rootDir);
|
|
2959
3289
|
const target = options.target ?? "mcp";
|
|
3290
|
+
const host = options.host ?? "node";
|
|
2960
3291
|
const config = analyzeProjectConfig(rootDir);
|
|
2961
3292
|
const tools = await analyzeProjectTools(rootDir, { target });
|
|
2962
3293
|
const resources = await analyzeProjectResources(rootDir);
|
|
2963
3294
|
const resourceTemplates = [];
|
|
2964
3295
|
const prompts = await analyzeProjectPrompts(rootDir);
|
|
3296
|
+
const identity = await loadProjectIdentity(rootDir);
|
|
2965
3297
|
const diagnostics = await collectProjectDiagnostics(rootDir, {
|
|
2966
3298
|
tools,
|
|
2967
3299
|
resources,
|
|
@@ -2977,6 +3309,7 @@ async function buildProject(options) {
|
|
|
2977
3309
|
const manifest = {
|
|
2978
3310
|
version: 1,
|
|
2979
3311
|
target,
|
|
3312
|
+
host,
|
|
2980
3313
|
rootDir: ".",
|
|
2981
3314
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2982
3315
|
config,
|
|
@@ -2986,23 +3319,24 @@ async function buildProject(options) {
|
|
|
2986
3319
|
prompts,
|
|
2987
3320
|
diagnostics
|
|
2988
3321
|
};
|
|
2989
|
-
await
|
|
2990
|
-
await
|
|
2991
|
-
|
|
3322
|
+
await mkdir9(outDir, { recursive: true });
|
|
3323
|
+
await writeFile9(
|
|
3324
|
+
path18.join(outDir, "manifest.sidecar.json"),
|
|
2992
3325
|
`${JSON.stringify(manifest, null, 2)}
|
|
2993
3326
|
`
|
|
2994
3327
|
);
|
|
2995
|
-
await
|
|
3328
|
+
await writeFile9(path18.join(outDir, "README.md"), renderMcpReadme(manifest));
|
|
2996
3329
|
await writeGeneratedTypes(rootDir, tools);
|
|
3330
|
+
await buildServerOutput(rootDir, outDir, manifest, identity, host);
|
|
2997
3331
|
if (options.plugins) {
|
|
2998
|
-
await buildPluginPackages(rootDir,
|
|
3332
|
+
await buildPluginPackages(rootDir, path18.dirname(outDir), manifest);
|
|
2999
3333
|
}
|
|
3000
3334
|
return manifest;
|
|
3001
3335
|
}
|
|
3002
3336
|
function resolveInsideRoot(rootDir, output) {
|
|
3003
|
-
const resolved =
|
|
3004
|
-
const relative =
|
|
3005
|
-
if (relative.startsWith("..") ||
|
|
3337
|
+
const resolved = path18.resolve(rootDir, output);
|
|
3338
|
+
const relative = path18.relative(rootDir, resolved);
|
|
3339
|
+
if (relative.startsWith("..") || path18.isAbsolute(relative)) {
|
|
3006
3340
|
throw new Error(`Build output must stay inside the project root: ${output}`);
|
|
3007
3341
|
}
|
|
3008
3342
|
return resolved;
|
|
@@ -3027,9 +3361,35 @@ ${resources || "No resources detected."}
|
|
|
3027
3361
|
|
|
3028
3362
|
${prompts || "No prompts detected."}
|
|
3029
3363
|
|
|
3364
|
+
${manifest.host === "vercel" ? renderVercelReadmeSection() : renderNodeReadmeSection()}
|
|
3365
|
+
|
|
3030
3366
|
## Local HTTPS Testing
|
|
3031
3367
|
|
|
3032
|
-
Run \`sidecar dev --tunnel\` from the project root to start the local MCP server on Streamable HTTP and print
|
|
3368
|
+
Run \`sidecar dev --tunnel\` from the project root to start the local MCP server on Streamable HTTP and print a validated HTTPS MCP URL that can be added to ChatGPT, Claude, or a Claude plugin install. Temporary quick tunnels are public and best-effort; use a configured tunnel/domain or deployed preview for repeatable testing.
|
|
3369
|
+
`;
|
|
3370
|
+
}
|
|
3371
|
+
function renderNodeReadmeSection() {
|
|
3372
|
+
return `## Run The Server
|
|
3373
|
+
|
|
3374
|
+
This output includes a standalone Node MCP server. Start it with:
|
|
3375
|
+
|
|
3376
|
+
\`\`\`sh
|
|
3377
|
+
npm start
|
|
3378
|
+
\`\`\`
|
|
3379
|
+
|
|
3380
|
+
or:
|
|
3381
|
+
|
|
3382
|
+
\`\`\`sh
|
|
3383
|
+
node server/index.js
|
|
3384
|
+
\`\`\`
|
|
3385
|
+
|
|
3386
|
+
Set \`PORT\` or \`SIDECAR_PORT\` to choose the listen port. Hosted/authenticated MCPs should set \`SIDECAR_MCP_URL\` to the public \`https://.../mcp\` URL before starting.
|
|
3387
|
+
`;
|
|
3388
|
+
}
|
|
3389
|
+
function renderVercelReadmeSection() {
|
|
3390
|
+
return `## Deploy To Vercel
|
|
3391
|
+
|
|
3392
|
+
This output includes a Vercel Function shim at \`api/sidecar.js\` and a \`vercel.json\` rewrite that sends Streamable HTTP traffic to \`/mcp\`. Deploy this output directory with Vercel and set \`SIDECAR_MCP_URL\` to the public \`https://.../mcp\` URL.
|
|
3033
3393
|
`;
|
|
3034
3394
|
}
|
|
3035
3395
|
|
|
@@ -3042,7 +3402,7 @@ import { randomUUID } from "crypto";
|
|
|
3042
3402
|
var proxyBrand = /* @__PURE__ */ Symbol.for("sidecar.proxy");
|
|
3043
3403
|
function isSidecarProxy(value) {
|
|
3044
3404
|
return Boolean(
|
|
3045
|
-
value && typeof value === "object" && value[proxyBrand] === true
|
|
3405
|
+
value && typeof value === "object" && (value[proxyBrand] === true || value.kind === "sidecar.proxy")
|
|
3046
3406
|
);
|
|
3047
3407
|
}
|
|
3048
3408
|
async function runProxy(proxyConfig, request) {
|
|
@@ -3055,8 +3415,90 @@ async function runProxy(proxyConfig, request) {
|
|
|
3055
3415
|
return void 0;
|
|
3056
3416
|
}
|
|
3057
3417
|
|
|
3058
|
-
// packages/server/src/
|
|
3418
|
+
// packages/server/src/sse.ts
|
|
3059
3419
|
import { JSONRPC_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
3420
|
+
var sseEventCounter = 0;
|
|
3421
|
+
function createSseHub() {
|
|
3422
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3423
|
+
return {
|
|
3424
|
+
open(response) {
|
|
3425
|
+
const stream = createSseStream(response);
|
|
3426
|
+
streams.add(stream);
|
|
3427
|
+
response.on("close", () => {
|
|
3428
|
+
streams.delete(stream);
|
|
3429
|
+
});
|
|
3430
|
+
return stream;
|
|
3431
|
+
},
|
|
3432
|
+
send(method, params) {
|
|
3433
|
+
const stream = streams.values().next().value;
|
|
3434
|
+
stream?.send(method, params);
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
function createSseStream(response, options = {}) {
|
|
3439
|
+
let closed = false;
|
|
3440
|
+
response.writeHead(200, {
|
|
3441
|
+
"content-type": "text/event-stream",
|
|
3442
|
+
"cache-control": "no-cache, no-transform",
|
|
3443
|
+
"connection": "keep-alive",
|
|
3444
|
+
"x-accel-buffering": "no"
|
|
3445
|
+
});
|
|
3446
|
+
response.flushHeaders?.();
|
|
3447
|
+
const keepAlive = setInterval(() => {
|
|
3448
|
+
writeRaw(": keepalive\n\n");
|
|
3449
|
+
}, 3e4);
|
|
3450
|
+
keepAlive.unref?.();
|
|
3451
|
+
response.on("close", () => {
|
|
3452
|
+
closed = true;
|
|
3453
|
+
clearInterval(keepAlive);
|
|
3454
|
+
});
|
|
3455
|
+
writeRaw(`id: ${nextSseEventId()}
|
|
3456
|
+
data:
|
|
3457
|
+
|
|
3458
|
+
`);
|
|
3459
|
+
function writeRaw(frame) {
|
|
3460
|
+
if (!closed && !response.destroyed) {
|
|
3461
|
+
response.write(frame);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function sendJson(value) {
|
|
3465
|
+
writeRaw([
|
|
3466
|
+
`id: ${nextSseEventId()}`,
|
|
3467
|
+
"event: message",
|
|
3468
|
+
`data: ${JSON.stringify(value)}`,
|
|
3469
|
+
"",
|
|
3470
|
+
""
|
|
3471
|
+
].join("\n"));
|
|
3472
|
+
}
|
|
3473
|
+
return {
|
|
3474
|
+
supportsRequestProgress: options.supportsRequestProgress,
|
|
3475
|
+
send(method, params) {
|
|
3476
|
+
sendJson(omitUndefined({
|
|
3477
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3478
|
+
method,
|
|
3479
|
+
params
|
|
3480
|
+
}));
|
|
3481
|
+
},
|
|
3482
|
+
sendJson,
|
|
3483
|
+
end() {
|
|
3484
|
+
closed = true;
|
|
3485
|
+
clearInterval(keepAlive);
|
|
3486
|
+
response.end();
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
function nextSseEventId() {
|
|
3491
|
+
sseEventCounter += 1;
|
|
3492
|
+
return `sidecar-${Date.now()}-${sseEventCounter}`;
|
|
3493
|
+
}
|
|
3494
|
+
function omitUndefined(value) {
|
|
3495
|
+
return Object.fromEntries(
|
|
3496
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// packages/server/src/index.ts
|
|
3501
|
+
import { JSONRPC_VERSION as JSONRPC_VERSION2 } from "@modelcontextprotocol/sdk/types.js";
|
|
3060
3502
|
var SIDECAR_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
3061
3503
|
var SidecarMcpServer = class {
|
|
3062
3504
|
constructor(options) {
|
|
@@ -3100,6 +3542,8 @@ var SidecarMcpServer = class {
|
|
|
3100
3542
|
resources = /* @__PURE__ */ new Map();
|
|
3101
3543
|
prompts = /* @__PURE__ */ new Map();
|
|
3102
3544
|
resourceTemplates;
|
|
3545
|
+
activeRequests = /* @__PURE__ */ new Map();
|
|
3546
|
+
subscribedResourceUris = /* @__PURE__ */ new Set();
|
|
3103
3547
|
/** Returns all tool descriptors exposed through `tools/list`. */
|
|
3104
3548
|
descriptors() {
|
|
3105
3549
|
return [...this.tools.values()].map((loaded) => loaded.descriptor);
|
|
@@ -3124,13 +3568,16 @@ var SidecarMcpServer = class {
|
|
|
3124
3568
|
const authorizedContext = { ...context, authSession };
|
|
3125
3569
|
const result = await this.dispatch(request, authorizedContext);
|
|
3126
3570
|
return {
|
|
3127
|
-
jsonrpc:
|
|
3571
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3128
3572
|
id: request.id,
|
|
3129
3573
|
result
|
|
3130
3574
|
};
|
|
3131
3575
|
} catch (error) {
|
|
3576
|
+
if (error instanceof RequestCancelledError) {
|
|
3577
|
+
return void 0;
|
|
3578
|
+
}
|
|
3132
3579
|
return {
|
|
3133
|
-
jsonrpc:
|
|
3580
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3134
3581
|
id: request.id,
|
|
3135
3582
|
error: normalizeError(error)
|
|
3136
3583
|
};
|
|
@@ -3148,6 +3595,8 @@ var SidecarMcpServer = class {
|
|
|
3148
3595
|
version: this.options.version ?? "0.0.0-dev"
|
|
3149
3596
|
}
|
|
3150
3597
|
};
|
|
3598
|
+
case "ping":
|
|
3599
|
+
return {};
|
|
3151
3600
|
case "tools/list":
|
|
3152
3601
|
return this.listTools(request, context);
|
|
3153
3602
|
case "tools/call":
|
|
@@ -3260,29 +3709,39 @@ var SidecarMcpServer = class {
|
|
|
3260
3709
|
if (authSession !== void 0) {
|
|
3261
3710
|
ctx.auth = authSession;
|
|
3262
3711
|
}
|
|
3712
|
+
ctx.notify = this.createNotifications(context, request);
|
|
3263
3713
|
const controller = new AbortController();
|
|
3714
|
+
if (request.id !== null && request.id !== void 0) {
|
|
3715
|
+
this.activeRequests.set(request.id, controller);
|
|
3716
|
+
}
|
|
3264
3717
|
ctx.request = {
|
|
3265
3718
|
...ctx.request,
|
|
3266
3719
|
signal: controller.signal
|
|
3267
3720
|
};
|
|
3268
|
-
|
|
3269
|
-
loaded.
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
if (loaded.descriptorProvided) {
|
|
3279
|
-
validateAgainstSchema(
|
|
3280
|
-
loaded.descriptor.outputSchema,
|
|
3281
|
-
result.structuredContent,
|
|
3282
|
-
`Invalid structuredContent returned by tool "${name}".`
|
|
3721
|
+
try {
|
|
3722
|
+
const args = loaded.descriptorProvided ? validateAgainstSchema(
|
|
3723
|
+
loaded.descriptor.inputSchema,
|
|
3724
|
+
params?.arguments ?? {},
|
|
3725
|
+
`Invalid parameters for tool "${name}".`
|
|
3726
|
+
) : params?.arguments ?? {};
|
|
3727
|
+
const result = await withTimeout(
|
|
3728
|
+
executeTool(loaded.tool, args, ctx),
|
|
3729
|
+
this.options.toolTimeoutMs,
|
|
3730
|
+
controller
|
|
3283
3731
|
);
|
|
3732
|
+
if (loaded.descriptorProvided) {
|
|
3733
|
+
validateAgainstSchema(
|
|
3734
|
+
loaded.descriptor.outputSchema,
|
|
3735
|
+
result.structuredContent,
|
|
3736
|
+
`Invalid structuredContent returned by tool "${name}".`
|
|
3737
|
+
);
|
|
3738
|
+
}
|
|
3739
|
+
return result;
|
|
3740
|
+
} finally {
|
|
3741
|
+
if (request.id !== null && request.id !== void 0) {
|
|
3742
|
+
this.activeRequests.delete(request.id);
|
|
3743
|
+
}
|
|
3284
3744
|
}
|
|
3285
|
-
return result;
|
|
3286
3745
|
}
|
|
3287
3746
|
/** Authenticates the whole HTTP MCP request when auth.ts is configured. */
|
|
3288
3747
|
async authorizeEndpoint(context) {
|
|
@@ -3334,6 +3793,7 @@ var SidecarMcpServer = class {
|
|
|
3334
3793
|
if (context.authSession !== void 0) {
|
|
3335
3794
|
toolContext.auth = context.authSession;
|
|
3336
3795
|
}
|
|
3796
|
+
toolContext.notify = this.createNotifications(context, request);
|
|
3337
3797
|
return executeResource(resource.resource, toResourceContext(toolContext), {
|
|
3338
3798
|
uri,
|
|
3339
3799
|
mimeType: resource.descriptor.mimeType
|
|
@@ -3359,6 +3819,7 @@ var SidecarMcpServer = class {
|
|
|
3359
3819
|
if (!this.resources.has(uri)) {
|
|
3360
3820
|
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
3361
3821
|
}
|
|
3822
|
+
this.subscribedResourceUris.add(uri);
|
|
3362
3823
|
return {};
|
|
3363
3824
|
}
|
|
3364
3825
|
/** Accepts a resource unsubscription when server-level support is enabled. */
|
|
@@ -3370,6 +3831,7 @@ var SidecarMcpServer = class {
|
|
|
3370
3831
|
if (!this.resources.has(uri)) {
|
|
3371
3832
|
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
3372
3833
|
}
|
|
3834
|
+
this.subscribedResourceUris.delete(uri);
|
|
3373
3835
|
return {};
|
|
3374
3836
|
}
|
|
3375
3837
|
/** Renders one named prompt with validated arguments. */
|
|
@@ -3387,20 +3849,56 @@ var SidecarMcpServer = class {
|
|
|
3387
3849
|
if (context.authSession !== void 0) {
|
|
3388
3850
|
toolContext.auth = context.authSession;
|
|
3389
3851
|
}
|
|
3852
|
+
toolContext.notify = this.createNotifications(context, request);
|
|
3390
3853
|
return executePrompt(loaded.prompt, params?.arguments ?? {}, toPromptContext(toolContext));
|
|
3391
3854
|
}
|
|
3855
|
+
/** Creates notification helpers scoped by advertised server capabilities. */
|
|
3856
|
+
createNotifications(context, request) {
|
|
3857
|
+
const configured = this.options.capabilities ?? {};
|
|
3858
|
+
return createRuntimeNotifications(
|
|
3859
|
+
context.notifications,
|
|
3860
|
+
readProgressToken(request),
|
|
3861
|
+
{
|
|
3862
|
+
toolsListChanged: Boolean(configured.tools?.listChanged),
|
|
3863
|
+
resourcesListChanged: Boolean(configured.resources?.listChanged),
|
|
3864
|
+
promptsListChanged: Boolean(configured.prompts?.listChanged),
|
|
3865
|
+
isResourceSubscribed: (uri) => this.subscribedResourceUris.has(uri)
|
|
3866
|
+
}
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3392
3869
|
/** Accepts notifications without side effects for client compatibility. */
|
|
3393
|
-
async handleNotification(
|
|
3870
|
+
async handleNotification(request) {
|
|
3871
|
+
if (request.method === "notifications/cancelled") {
|
|
3872
|
+
this.cancelRequest(request);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
/** Applies a client cancellation notification to an active request. */
|
|
3876
|
+
cancelRequest(request) {
|
|
3877
|
+
const params = request.params;
|
|
3878
|
+
const requestId = params?.requestId;
|
|
3879
|
+
if (typeof requestId !== "string" && typeof requestId !== "number") {
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
const controller = this.activeRequests.get(requestId);
|
|
3883
|
+
if (!controller || controller.signal.aborted) {
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
const reason = typeof params?.reason === "string" ? params.reason : "Request cancelled.";
|
|
3887
|
+
controller.abort(new RequestCancelledError(reason));
|
|
3394
3888
|
}
|
|
3395
3889
|
};
|
|
3396
3890
|
function createSidecarMcpServer(options) {
|
|
3397
3891
|
return new SidecarMcpServer(options);
|
|
3398
3892
|
}
|
|
3399
3893
|
function createSidecarHttpServer(options) {
|
|
3400
|
-
|
|
3894
|
+
return createServer(createSidecarHttpHandler(options));
|
|
3895
|
+
}
|
|
3896
|
+
function createSidecarHttpHandler(options) {
|
|
3401
3897
|
const endpoint = options.path ?? "/mcp";
|
|
3402
3898
|
const maxBodyBytes = options.maxBodyBytes ?? 1e6;
|
|
3403
|
-
|
|
3899
|
+
const streamHub = createSseHub();
|
|
3900
|
+
const mcp = createSidecarMcpServer(options);
|
|
3901
|
+
return async (request, response) => {
|
|
3404
3902
|
if (isRejectedOrigin(request, options.allowedOrigins)) {
|
|
3405
3903
|
response.writeHead(403, { "content-type": "application/json" });
|
|
3406
3904
|
response.end(JSON.stringify({ error: "forbidden_origin" }));
|
|
@@ -3419,11 +3917,39 @@ function createSidecarHttpServer(options) {
|
|
|
3419
3917
|
return;
|
|
3420
3918
|
}
|
|
3421
3919
|
if (request.method === "GET" && pathname === endpoint) {
|
|
3422
|
-
|
|
3423
|
-
|
|
3920
|
+
try {
|
|
3921
|
+
validateProtocolVersion(request);
|
|
3922
|
+
validateGetHeaders(request);
|
|
3923
|
+
const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
|
|
3924
|
+
if (options.auth) {
|
|
3925
|
+
const authSession = await authorizeHttpRequest(options.auth, fetchRequest, response);
|
|
3926
|
+
if (authSession === AUTH_RESPONSE_SENT) {
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
streamHub.open(response);
|
|
3931
|
+
} catch (error) {
|
|
3932
|
+
const status = error instanceof JsonRpcHttpError ? error.status : 400;
|
|
3933
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
3934
|
+
response.end(
|
|
3935
|
+
JSON.stringify({
|
|
3936
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3937
|
+
id: null,
|
|
3938
|
+
error: normalizeHttpError(error)
|
|
3939
|
+
})
|
|
3940
|
+
);
|
|
3941
|
+
}
|
|
3424
3942
|
return;
|
|
3425
3943
|
}
|
|
3426
|
-
if (request.method !== "POST"
|
|
3944
|
+
if (pathname === endpoint && request.method !== "POST") {
|
|
3945
|
+
response.writeHead(405, {
|
|
3946
|
+
"allow": "GET, POST",
|
|
3947
|
+
"content-type": "application/json"
|
|
3948
|
+
});
|
|
3949
|
+
response.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (pathname !== endpoint) {
|
|
3427
3953
|
response.writeHead(404, { "content-type": "application/json" });
|
|
3428
3954
|
response.end(JSON.stringify({ error: "not_found" }));
|
|
3429
3955
|
return;
|
|
@@ -3446,9 +3972,23 @@ function createSidecarHttpServer(options) {
|
|
|
3446
3972
|
if (options.auth && authSession === AUTH_RESPONSE_SENT) {
|
|
3447
3973
|
return;
|
|
3448
3974
|
}
|
|
3975
|
+
if (rpcRequest.id !== void 0 && hasProgressToken(rpcRequest)) {
|
|
3976
|
+
const stream = createSseStream(response, { supportsRequestProgress: true });
|
|
3977
|
+
const payload2 = await mcp.handle(rpcRequest, {
|
|
3978
|
+
request: fetchRequest,
|
|
3979
|
+
authSession,
|
|
3980
|
+
notifications: stream
|
|
3981
|
+
});
|
|
3982
|
+
if (payload2) {
|
|
3983
|
+
stream.sendJson(payload2);
|
|
3984
|
+
}
|
|
3985
|
+
stream.end();
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3449
3988
|
const payload = await mcp.handle(rpcRequest, {
|
|
3450
3989
|
request: fetchRequest,
|
|
3451
|
-
authSession
|
|
3990
|
+
authSession,
|
|
3991
|
+
notifications: streamHub
|
|
3452
3992
|
}) ?? null;
|
|
3453
3993
|
const responses = payload === null ? [] : [payload];
|
|
3454
3994
|
if (!responses.length) {
|
|
@@ -3467,13 +4007,13 @@ function createSidecarHttpServer(options) {
|
|
|
3467
4007
|
response.writeHead(status, { "content-type": "application/json" });
|
|
3468
4008
|
response.end(
|
|
3469
4009
|
JSON.stringify({
|
|
3470
|
-
jsonrpc:
|
|
4010
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3471
4011
|
id: null,
|
|
3472
4012
|
error: normalizeHttpError(error)
|
|
3473
4013
|
})
|
|
3474
4014
|
);
|
|
3475
4015
|
}
|
|
3476
|
-
}
|
|
4016
|
+
};
|
|
3477
4017
|
}
|
|
3478
4018
|
var DEFAULT_ALLOWED_ORIGINS = [
|
|
3479
4019
|
"https://chatgpt.com",
|
|
@@ -3541,6 +4081,12 @@ var JsonRpcHttpError = class extends JsonRpcError {
|
|
|
3541
4081
|
}
|
|
3542
4082
|
status;
|
|
3543
4083
|
};
|
|
4084
|
+
var RequestCancelledError = class extends Error {
|
|
4085
|
+
constructor(message = "Request cancelled.") {
|
|
4086
|
+
super(message);
|
|
4087
|
+
this.name = "RequestCancelledError";
|
|
4088
|
+
}
|
|
4089
|
+
};
|
|
3544
4090
|
var AUTH_RESPONSE_SENT = /* @__PURE__ */ Symbol("sidecar.auth.response-sent");
|
|
3545
4091
|
async function authorizeHttpRequest(auth, request, response) {
|
|
3546
4092
|
const result = await auth.authorizeRequest(request);
|
|
@@ -3553,7 +4099,7 @@ async function authorizeHttpRequest(auth, request, response) {
|
|
|
3553
4099
|
});
|
|
3554
4100
|
response.end(
|
|
3555
4101
|
JSON.stringify({
|
|
3556
|
-
jsonrpc:
|
|
4102
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3557
4103
|
id: null,
|
|
3558
4104
|
error: {
|
|
3559
4105
|
code: result.status === 401 ? -32001 : -32003,
|
|
@@ -3596,9 +4142,57 @@ function createDefaultContext(request) {
|
|
|
3596
4142
|
memory.delete(key);
|
|
3597
4143
|
}
|
|
3598
4144
|
},
|
|
4145
|
+
notify: noopNotifications,
|
|
3599
4146
|
env: process.env
|
|
3600
4147
|
};
|
|
3601
4148
|
}
|
|
4149
|
+
var noopNotifications = {
|
|
4150
|
+
async progress() {
|
|
4151
|
+
},
|
|
4152
|
+
async toolsChanged() {
|
|
4153
|
+
},
|
|
4154
|
+
async resourcesChanged() {
|
|
4155
|
+
},
|
|
4156
|
+
async promptsChanged() {
|
|
4157
|
+
},
|
|
4158
|
+
async resourceUpdated() {
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
function createRuntimeNotifications(sink, progressToken, options) {
|
|
4162
|
+
return {
|
|
4163
|
+
async progress(update) {
|
|
4164
|
+
if (!sink?.supportsRequestProgress || progressToken === void 0) {
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
sink.send("notifications/progress", stripUndefined4({
|
|
4168
|
+
progressToken,
|
|
4169
|
+
progress: update.progress,
|
|
4170
|
+
total: update.total,
|
|
4171
|
+
message: update.message
|
|
4172
|
+
}));
|
|
4173
|
+
},
|
|
4174
|
+
async toolsChanged() {
|
|
4175
|
+
if (options.toolsListChanged) {
|
|
4176
|
+
sink?.send("notifications/tools/list_changed");
|
|
4177
|
+
}
|
|
4178
|
+
},
|
|
4179
|
+
async resourcesChanged() {
|
|
4180
|
+
if (options.resourcesListChanged) {
|
|
4181
|
+
sink?.send("notifications/resources/list_changed");
|
|
4182
|
+
}
|
|
4183
|
+
},
|
|
4184
|
+
async promptsChanged() {
|
|
4185
|
+
if (options.promptsListChanged) {
|
|
4186
|
+
sink?.send("notifications/prompts/list_changed");
|
|
4187
|
+
}
|
|
4188
|
+
},
|
|
4189
|
+
async resourceUpdated(uri) {
|
|
4190
|
+
if (options.isResourceSubscribed(uri)) {
|
|
4191
|
+
sink?.send("notifications/resources/updated", { uri });
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
3602
4196
|
function toResourceContext(ctx) {
|
|
3603
4197
|
return {
|
|
3604
4198
|
auth: ctx.auth,
|
|
@@ -3606,6 +4200,7 @@ function toResourceContext(ctx) {
|
|
|
3606
4200
|
services: ctx.services,
|
|
3607
4201
|
log: ctx.log,
|
|
3608
4202
|
storage: ctx.storage,
|
|
4203
|
+
notify: ctx.notify,
|
|
3609
4204
|
env: ctx.env
|
|
3610
4205
|
};
|
|
3611
4206
|
}
|
|
@@ -3616,6 +4211,7 @@ function toPromptContext(ctx) {
|
|
|
3616
4211
|
services: ctx.services,
|
|
3617
4212
|
log: ctx.log,
|
|
3618
4213
|
storage: ctx.storage,
|
|
4214
|
+
notify: ctx.notify,
|
|
3619
4215
|
env: ctx.env
|
|
3620
4216
|
};
|
|
3621
4217
|
}
|
|
@@ -3637,6 +4233,27 @@ function readUriParam(request, method) {
|
|
|
3637
4233
|
}
|
|
3638
4234
|
return uri;
|
|
3639
4235
|
}
|
|
4236
|
+
function readProgressToken(request) {
|
|
4237
|
+
const params = request.params;
|
|
4238
|
+
if (!params || typeof params !== "object") {
|
|
4239
|
+
return void 0;
|
|
4240
|
+
}
|
|
4241
|
+
const meta = params._meta;
|
|
4242
|
+
if (!meta || typeof meta !== "object") {
|
|
4243
|
+
return void 0;
|
|
4244
|
+
}
|
|
4245
|
+
const progressToken = meta.progressToken;
|
|
4246
|
+
if (typeof progressToken === "string") {
|
|
4247
|
+
return progressToken;
|
|
4248
|
+
}
|
|
4249
|
+
if (typeof progressToken === "number" && Number.isInteger(progressToken)) {
|
|
4250
|
+
return progressToken;
|
|
4251
|
+
}
|
|
4252
|
+
return void 0;
|
|
4253
|
+
}
|
|
4254
|
+
function hasProgressToken(request) {
|
|
4255
|
+
return readProgressToken(request) !== void 0;
|
|
4256
|
+
}
|
|
3640
4257
|
function selectPaginationOverride(override, operation) {
|
|
3641
4258
|
if (!override) {
|
|
3642
4259
|
return void 0;
|
|
@@ -3725,18 +4342,48 @@ function validatePostHeaders(request) {
|
|
|
3725
4342
|
);
|
|
3726
4343
|
}
|
|
3727
4344
|
}
|
|
4345
|
+
function validateGetHeaders(request) {
|
|
4346
|
+
const accept = request.headers.accept;
|
|
4347
|
+
const acceptValue = Array.isArray(accept) ? accept.join(",") : accept;
|
|
4348
|
+
if (!acceptValue || !acceptValue.toLowerCase().includes("text/event-stream")) {
|
|
4349
|
+
throw new JsonRpcHttpError(
|
|
4350
|
+
406,
|
|
4351
|
+
-32600,
|
|
4352
|
+
"GET Accept must include text/event-stream."
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
3728
4356
|
async function withTimeout(promise, timeoutMs, controller) {
|
|
4357
|
+
let abortListener;
|
|
4358
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
4359
|
+
abortListener = () => {
|
|
4360
|
+
reject(abortReason(controller.signal.reason));
|
|
4361
|
+
};
|
|
4362
|
+
if (controller.signal.aborted) {
|
|
4363
|
+
abortListener();
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
controller.signal.addEventListener("abort", abortListener, { once: true });
|
|
4367
|
+
});
|
|
3729
4368
|
if (!timeoutMs || timeoutMs <= 0) {
|
|
3730
|
-
|
|
4369
|
+
try {
|
|
4370
|
+
return await Promise.race([promise, abortPromise]);
|
|
4371
|
+
} finally {
|
|
4372
|
+
if (abortListener) {
|
|
4373
|
+
controller.signal.removeEventListener("abort", abortListener);
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
3731
4376
|
}
|
|
3732
4377
|
let timeout;
|
|
3733
4378
|
try {
|
|
3734
4379
|
return await Promise.race([
|
|
3735
4380
|
promise,
|
|
4381
|
+
abortPromise,
|
|
3736
4382
|
new Promise((_resolve, reject) => {
|
|
3737
4383
|
timeout = setTimeout(() => {
|
|
3738
|
-
|
|
3739
|
-
|
|
4384
|
+
const error = new JsonRpcError(-32e3, "Tool execution timed out.");
|
|
4385
|
+
controller.abort(error);
|
|
4386
|
+
reject(error);
|
|
3740
4387
|
}, timeoutMs);
|
|
3741
4388
|
})
|
|
3742
4389
|
]);
|
|
@@ -3744,7 +4391,16 @@ async function withTimeout(promise, timeoutMs, controller) {
|
|
|
3744
4391
|
if (timeout) {
|
|
3745
4392
|
clearTimeout(timeout);
|
|
3746
4393
|
}
|
|
4394
|
+
if (abortListener) {
|
|
4395
|
+
controller.signal.removeEventListener("abort", abortListener);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
function abortReason(reason) {
|
|
4400
|
+
if (reason instanceof Error) {
|
|
4401
|
+
return reason;
|
|
3747
4402
|
}
|
|
4403
|
+
return new RequestCancelledError();
|
|
3748
4404
|
}
|
|
3749
4405
|
function validateAgainstSchema(schema, value, message, options = {}) {
|
|
3750
4406
|
if (!schema || value === void 0 && options.optional) {
|
|
@@ -3756,80 +4412,87 @@ function validateAgainstSchema(schema, value, message, options = {}) {
|
|
|
3756
4412
|
}
|
|
3757
4413
|
return value;
|
|
3758
4414
|
}
|
|
3759
|
-
function schemaFailure(schema, value,
|
|
3760
|
-
if (schema.anyOf?.length && !schema.anyOf.some((entry) => !schemaFailure(entry, value,
|
|
3761
|
-
return `${
|
|
4415
|
+
function schemaFailure(schema, value, path20) {
|
|
4416
|
+
if (schema.anyOf?.length && !schema.anyOf.some((entry) => !schemaFailure(entry, value, path20))) {
|
|
4417
|
+
return `${path20} must match one anyOf schema.`;
|
|
3762
4418
|
}
|
|
3763
|
-
if (schema.oneOf?.length && schema.oneOf.filter((entry) => !schemaFailure(entry, value,
|
|
3764
|
-
return `${
|
|
4419
|
+
if (schema.oneOf?.length && schema.oneOf.filter((entry) => !schemaFailure(entry, value, path20)).length !== 1) {
|
|
4420
|
+
return `${path20} must match exactly one oneOf schema.`;
|
|
3765
4421
|
}
|
|
3766
4422
|
if (schema.allOf?.length) {
|
|
3767
4423
|
for (const entry of schema.allOf) {
|
|
3768
|
-
const failure = schemaFailure(entry, value,
|
|
4424
|
+
const failure = schemaFailure(entry, value, path20);
|
|
3769
4425
|
if (failure) return failure;
|
|
3770
4426
|
}
|
|
3771
4427
|
}
|
|
3772
4428
|
if (schema.const !== void 0 && value !== schema.const) {
|
|
3773
|
-
return `${
|
|
4429
|
+
return `${path20} must equal ${JSON.stringify(schema.const)}.`;
|
|
3774
4430
|
}
|
|
3775
4431
|
if (schema.enum && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) {
|
|
3776
|
-
return `${
|
|
4432
|
+
return `${path20} must be one of the declared enum values.`;
|
|
3777
4433
|
}
|
|
3778
4434
|
if (schema.type) {
|
|
3779
4435
|
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
3780
4436
|
if (!types.some((type) => matchesJsonSchemaType(type, value))) {
|
|
3781
|
-
return `${
|
|
4437
|
+
return `${path20} must be ${types.join(" or ")}.`;
|
|
3782
4438
|
}
|
|
3783
4439
|
}
|
|
3784
4440
|
if (schema.type === "object" || schema.properties || schema.required) {
|
|
3785
4441
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3786
|
-
return `${
|
|
4442
|
+
return `${path20} must be an object.`;
|
|
3787
4443
|
}
|
|
3788
4444
|
const record = value;
|
|
3789
4445
|
for (const required of schema.required ?? []) {
|
|
3790
4446
|
if (!(required in record)) {
|
|
3791
|
-
return `${
|
|
4447
|
+
return `${path20}.${required} is required.`;
|
|
3792
4448
|
}
|
|
3793
4449
|
}
|
|
3794
4450
|
for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
3795
4451
|
if (key in record) {
|
|
3796
|
-
const failure = schemaFailure(propertySchema, record[key], `${
|
|
4452
|
+
const failure = schemaFailure(propertySchema, record[key], `${path20}.${key}`);
|
|
3797
4453
|
if (failure) return failure;
|
|
3798
4454
|
}
|
|
3799
4455
|
}
|
|
4456
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
3800
4457
|
if (schema.additionalProperties === false) {
|
|
3801
|
-
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
3802
4458
|
const extra = Object.keys(record).find((key) => !allowed.has(key));
|
|
3803
4459
|
if (extra) {
|
|
3804
|
-
return `${
|
|
4460
|
+
return `${path20}.${extra} is not allowed.`;
|
|
4461
|
+
}
|
|
4462
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
4463
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
4464
|
+
if (!allowed.has(key)) {
|
|
4465
|
+
const failure = schemaFailure(schema.additionalProperties, entry, `${path20}.${key}`);
|
|
4466
|
+
if (failure) return failure;
|
|
4467
|
+
}
|
|
3805
4468
|
}
|
|
3806
4469
|
}
|
|
3807
4470
|
}
|
|
3808
4471
|
if (schema.type === "array" || schema.items) {
|
|
3809
4472
|
if (!Array.isArray(value)) {
|
|
3810
|
-
return `${
|
|
4473
|
+
return `${path20} must be an array.`;
|
|
3811
4474
|
}
|
|
3812
4475
|
if (schema.items) {
|
|
3813
4476
|
for (const [index, entry] of value.entries()) {
|
|
3814
|
-
const failure = schemaFailure(schema.items, entry, `${
|
|
4477
|
+
const failure = schemaFailure(schema.items, entry, `${path20}[${index}]`);
|
|
3815
4478
|
if (failure) return failure;
|
|
3816
4479
|
}
|
|
3817
4480
|
}
|
|
3818
4481
|
}
|
|
3819
4482
|
if (typeof value === "string") {
|
|
3820
4483
|
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
3821
|
-
return `${
|
|
4484
|
+
return `${path20} is shorter than ${schema.minLength}.`;
|
|
3822
4485
|
}
|
|
3823
4486
|
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
3824
|
-
return `${
|
|
4487
|
+
return `${path20} is longer than ${schema.maxLength}.`;
|
|
3825
4488
|
}
|
|
3826
4489
|
}
|
|
3827
4490
|
if (typeof value === "number") {
|
|
3828
4491
|
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
3829
|
-
return `${
|
|
4492
|
+
return `${path20} is less than ${schema.minimum}.`;
|
|
3830
4493
|
}
|
|
3831
4494
|
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
3832
|
-
return `${
|
|
4495
|
+
return `${path20} is greater than ${schema.maximum}.`;
|
|
3833
4496
|
}
|
|
3834
4497
|
}
|
|
3835
4498
|
return void 0;
|
|
@@ -3937,14 +4600,14 @@ function assertJsonRpcRequest(value) {
|
|
|
3937
4600
|
throw new JsonRpcError(-32600, "Request must be a JSON object.");
|
|
3938
4601
|
}
|
|
3939
4602
|
const record = value;
|
|
3940
|
-
if (record.jsonrpc !==
|
|
4603
|
+
if (record.jsonrpc !== JSONRPC_VERSION2 || typeof record.method !== "string") {
|
|
3941
4604
|
throw new JsonRpcError(-32600, "Invalid JSON-RPC request.");
|
|
3942
4605
|
}
|
|
3943
4606
|
if ("id" in record && record.id !== void 0 && record.id !== null && typeof record.id !== "string" && typeof record.id !== "number") {
|
|
3944
4607
|
throw new JsonRpcError(-32600, "JSON-RPC id must be a string, number, or null.");
|
|
3945
4608
|
}
|
|
3946
4609
|
return {
|
|
3947
|
-
jsonrpc:
|
|
4610
|
+
jsonrpc: JSONRPC_VERSION2,
|
|
3948
4611
|
id: typeof record.id === "string" || typeof record.id === "number" || record.id === null ? record.id : void 0,
|
|
3949
4612
|
method: record.method,
|
|
3950
4613
|
params: record.params
|
|
@@ -3955,7 +4618,7 @@ function isJsonRpcResponseMessage(value) {
|
|
|
3955
4618
|
return false;
|
|
3956
4619
|
}
|
|
3957
4620
|
const record = value;
|
|
3958
|
-
if (record.jsonrpc !==
|
|
4621
|
+
if (record.jsonrpc !== JSONRPC_VERSION2 || !("id" in record) || !("result" in record || "error" in record)) {
|
|
3959
4622
|
return false;
|
|
3960
4623
|
}
|
|
3961
4624
|
return record.id === null || typeof record.id === "string" || typeof record.id === "number";
|
|
@@ -4007,11 +4670,31 @@ import { stdin, stdout } from "process";
|
|
|
4007
4670
|
async function startTunnel(options) {
|
|
4008
4671
|
const provider = await resolveProvider(options.provider);
|
|
4009
4672
|
const localUrl = `http://127.0.0.1:${options.port}`;
|
|
4010
|
-
const
|
|
4673
|
+
const path20 = options.path ?? "/mcp";
|
|
4011
4674
|
if (provider === "cloudflared") {
|
|
4012
|
-
return startCloudflared(localUrl,
|
|
4013
|
-
}
|
|
4014
|
-
return startWrangler(localUrl,
|
|
4675
|
+
return startCloudflared(localUrl, path20, options.timeoutMs);
|
|
4676
|
+
}
|
|
4677
|
+
return startWrangler(localUrl, path20, options.timeoutMs);
|
|
4678
|
+
}
|
|
4679
|
+
async function validateTunnelEndpoint(options) {
|
|
4680
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
4681
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
4682
|
+
if ((options.requireHttps ?? true) && mcpUrl.protocol !== "https:") {
|
|
4683
|
+
throw new Error(`Tunnel URL must use https://, received ${options.mcpUrl}.`);
|
|
4684
|
+
}
|
|
4685
|
+
await retryTunnelValidation(async () => {
|
|
4686
|
+
if (options.auth) {
|
|
4687
|
+
await validateProtectedResourceMetadata({
|
|
4688
|
+
mcpUrl,
|
|
4689
|
+
expectedResource: options.expectedResource ?? options.mcpUrl,
|
|
4690
|
+
timeoutMs
|
|
4691
|
+
});
|
|
4692
|
+
await validateAuthChallenge({ mcpUrl, timeoutMs });
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
await validateInitialize({ mcpUrl, timeoutMs });
|
|
4696
|
+
await validateToolsList({ mcpUrl, timeoutMs });
|
|
4697
|
+
}, timeoutMs);
|
|
4015
4698
|
}
|
|
4016
4699
|
function tunnelInstallMessage(provider = "auto") {
|
|
4017
4700
|
if (provider === "wrangler") {
|
|
@@ -4115,7 +4798,7 @@ function assertNpxAvailable() {
|
|
|
4115
4798
|
throw new Error(tunnelInstallMessage("wrangler"));
|
|
4116
4799
|
}
|
|
4117
4800
|
}
|
|
4118
|
-
async function startCloudflared(localUrl,
|
|
4801
|
+
async function startCloudflared(localUrl, path20, timeoutMs = 2e4) {
|
|
4119
4802
|
const child = spawn("cloudflared", ["tunnel", "--url", localUrl, "--no-autoupdate"], {
|
|
4120
4803
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4121
4804
|
});
|
|
@@ -4123,13 +4806,13 @@ async function startCloudflared(localUrl, path19, timeoutMs = 2e4) {
|
|
|
4123
4806
|
return {
|
|
4124
4807
|
provider: "cloudflared",
|
|
4125
4808
|
publicUrl,
|
|
4126
|
-
mcpUrl: appendPath(publicUrl,
|
|
4809
|
+
mcpUrl: appendPath(publicUrl, path20),
|
|
4127
4810
|
close() {
|
|
4128
4811
|
child.kill("SIGTERM");
|
|
4129
4812
|
}
|
|
4130
4813
|
};
|
|
4131
4814
|
}
|
|
4132
|
-
async function startWrangler(localUrl,
|
|
4815
|
+
async function startWrangler(localUrl, path20, timeoutMs = 2e4) {
|
|
4133
4816
|
const child = spawn("npx", ["--yes", "wrangler", "tunnel", "quick-start", localUrl], {
|
|
4134
4817
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4135
4818
|
});
|
|
@@ -4137,7 +4820,7 @@ async function startWrangler(localUrl, path19, timeoutMs = 2e4) {
|
|
|
4137
4820
|
return {
|
|
4138
4821
|
provider: "wrangler",
|
|
4139
4822
|
publicUrl,
|
|
4140
|
-
mcpUrl: appendPath(publicUrl,
|
|
4823
|
+
mcpUrl: appendPath(publicUrl, path20),
|
|
4141
4824
|
close() {
|
|
4142
4825
|
child.kill("SIGTERM");
|
|
4143
4826
|
}
|
|
@@ -4146,15 +4829,20 @@ async function startWrangler(localUrl, path19, timeoutMs = 2e4) {
|
|
|
4146
4829
|
function waitForTunnelUrl(child, pattern, timeoutMs) {
|
|
4147
4830
|
return new Promise((resolve, reject) => {
|
|
4148
4831
|
let settled = false;
|
|
4832
|
+
let output = "";
|
|
4149
4833
|
const timeout = setTimeout(() => {
|
|
4150
4834
|
if (!settled) {
|
|
4151
4835
|
settled = true;
|
|
4152
4836
|
child.kill("SIGTERM");
|
|
4153
|
-
reject(new Error(
|
|
4837
|
+
reject(new Error([
|
|
4838
|
+
"Timed out waiting for HTTPS tunnel URL.",
|
|
4839
|
+
tunnelOutputHint(output)
|
|
4840
|
+
].filter(Boolean).join("\n")));
|
|
4154
4841
|
}
|
|
4155
4842
|
}, timeoutMs);
|
|
4156
4843
|
const inspect = (chunk) => {
|
|
4157
|
-
|
|
4844
|
+
output = tail(`${output}${chunk.toString("utf8")}`, 4e3);
|
|
4845
|
+
const match = output.match(pattern);
|
|
4158
4846
|
if (match?.[0] && !settled) {
|
|
4159
4847
|
settled = true;
|
|
4160
4848
|
clearTimeout(timeout);
|
|
@@ -4174,11 +4862,209 @@ function waitForTunnelUrl(child, pattern, timeoutMs) {
|
|
|
4174
4862
|
if (!settled) {
|
|
4175
4863
|
settled = true;
|
|
4176
4864
|
clearTimeout(timeout);
|
|
4177
|
-
reject(new Error(
|
|
4865
|
+
reject(new Error([
|
|
4866
|
+
`Tunnel process exited before producing a URL${code === null ? "" : ` with code ${code}`}.`,
|
|
4867
|
+
tunnelOutputHint(output)
|
|
4868
|
+
].filter(Boolean).join("\n")));
|
|
4178
4869
|
}
|
|
4179
4870
|
});
|
|
4180
4871
|
});
|
|
4181
4872
|
}
|
|
4873
|
+
async function validateProtectedResourceMetadata(options) {
|
|
4874
|
+
const metadataUrl = protectedResourceMetadataUrl(options.mcpUrl);
|
|
4875
|
+
const response = await fetchWithTimeout(metadataUrl, {
|
|
4876
|
+
headers: { accept: "application/json" },
|
|
4877
|
+
timeoutMs: options.timeoutMs
|
|
4878
|
+
});
|
|
4879
|
+
const body = await readValidationBody(response);
|
|
4880
|
+
assertNotHtml(response, body, metadataUrl);
|
|
4881
|
+
if (response.status !== 200) {
|
|
4882
|
+
if (isTransientHttpStatus(response.status)) {
|
|
4883
|
+
throw new TransientTunnelValidationError(`Tunnel validation failed: ${metadataUrl} returned HTTP ${response.status}.`);
|
|
4884
|
+
}
|
|
4885
|
+
throw new Error(`Tunnel validation failed: ${metadataUrl} returned HTTP ${response.status}.`);
|
|
4886
|
+
}
|
|
4887
|
+
const metadata = parseJsonObject(body, metadataUrl);
|
|
4888
|
+
if (metadata.resource !== options.expectedResource) {
|
|
4889
|
+
throw new Error([
|
|
4890
|
+
"Tunnel validation failed: OAuth protected-resource metadata does not match the public MCP URL.",
|
|
4891
|
+
` expected resource: ${options.expectedResource}`,
|
|
4892
|
+
` actual resource: ${String(metadata.resource)}`
|
|
4893
|
+
].join("\n"));
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
async function validateAuthChallenge(options) {
|
|
4897
|
+
const response = await postJsonRpc(options.mcpUrl, {
|
|
4898
|
+
jsonrpc: "2.0",
|
|
4899
|
+
id: 1,
|
|
4900
|
+
method: "initialize",
|
|
4901
|
+
params: {
|
|
4902
|
+
protocolVersion: "2025-11-25",
|
|
4903
|
+
capabilities: {},
|
|
4904
|
+
clientInfo: { name: "sidecar-tunnel-check", version: "0.0.0" }
|
|
4905
|
+
}
|
|
4906
|
+
}, options.timeoutMs);
|
|
4907
|
+
const body = await readValidationBody(response);
|
|
4908
|
+
assertNotHtml(response, body, options.mcpUrl.toString());
|
|
4909
|
+
if (response.status !== 401) {
|
|
4910
|
+
if (isTransientHttpStatus(response.status)) {
|
|
4911
|
+
throw new TransientTunnelValidationError(`Tunnel validation failed: authenticated MCP endpoint returned HTTP ${response.status}.`);
|
|
4912
|
+
}
|
|
4913
|
+
throw new Error(`Tunnel validation failed: authenticated MCP endpoint should return HTTP 401 without a bearer token, received HTTP ${response.status}.`);
|
|
4914
|
+
}
|
|
4915
|
+
if (!response.headers.get("www-authenticate")?.toLowerCase().includes("bearer")) {
|
|
4916
|
+
throw new Error("Tunnel validation failed: authenticated MCP endpoint did not include a Bearer WWW-Authenticate challenge.");
|
|
4917
|
+
}
|
|
4918
|
+
parseJsonObject(body, options.mcpUrl.toString());
|
|
4919
|
+
}
|
|
4920
|
+
async function validateInitialize(options) {
|
|
4921
|
+
const response = await postJsonRpc(options.mcpUrl, {
|
|
4922
|
+
jsonrpc: "2.0",
|
|
4923
|
+
id: 1,
|
|
4924
|
+
method: "initialize",
|
|
4925
|
+
params: {
|
|
4926
|
+
protocolVersion: "2025-11-25",
|
|
4927
|
+
capabilities: {},
|
|
4928
|
+
clientInfo: { name: "sidecar-tunnel-check", version: "0.0.0" }
|
|
4929
|
+
}
|
|
4930
|
+
}, options.timeoutMs);
|
|
4931
|
+
const body = await readValidationBody(response);
|
|
4932
|
+
assertNotHtml(response, body, options.mcpUrl.toString());
|
|
4933
|
+
if (response.status !== 200) {
|
|
4934
|
+
if (isTransientHttpStatus(response.status)) {
|
|
4935
|
+
throw new TransientTunnelValidationError(`Tunnel validation failed: initialize returned HTTP ${response.status}.`);
|
|
4936
|
+
}
|
|
4937
|
+
throw new Error(`Tunnel validation failed: initialize returned HTTP ${response.status}.`);
|
|
4938
|
+
}
|
|
4939
|
+
const payload = parseJsonObject(body, options.mcpUrl.toString());
|
|
4940
|
+
if (!isRecord3(payload.result) || typeof payload.result.protocolVersion !== "string") {
|
|
4941
|
+
throw new Error("Tunnel validation failed: initialize did not return an MCP server result.");
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
async function validateToolsList(options) {
|
|
4945
|
+
const response = await postJsonRpc(options.mcpUrl, {
|
|
4946
|
+
jsonrpc: "2.0",
|
|
4947
|
+
id: 2,
|
|
4948
|
+
method: "tools/list"
|
|
4949
|
+
}, options.timeoutMs);
|
|
4950
|
+
const body = await readValidationBody(response);
|
|
4951
|
+
assertNotHtml(response, body, options.mcpUrl.toString());
|
|
4952
|
+
if (response.status !== 200) {
|
|
4953
|
+
if (isTransientHttpStatus(response.status)) {
|
|
4954
|
+
throw new TransientTunnelValidationError(`Tunnel validation failed: tools/list returned HTTP ${response.status}.`);
|
|
4955
|
+
}
|
|
4956
|
+
throw new Error(`Tunnel validation failed: tools/list returned HTTP ${response.status}.`);
|
|
4957
|
+
}
|
|
4958
|
+
const payload = parseJsonObject(body, options.mcpUrl.toString());
|
|
4959
|
+
if (!isRecord3(payload.result) || !Array.isArray(payload.result.tools)) {
|
|
4960
|
+
throw new Error("Tunnel validation failed: tools/list did not return a tools array.");
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
function postJsonRpc(url, payload, timeoutMs) {
|
|
4964
|
+
return fetchWithTimeout(url.toString(), {
|
|
4965
|
+
method: "POST",
|
|
4966
|
+
headers: {
|
|
4967
|
+
accept: "application/json, text/event-stream",
|
|
4968
|
+
"content-type": "application/json"
|
|
4969
|
+
},
|
|
4970
|
+
body: JSON.stringify(payload),
|
|
4971
|
+
timeoutMs
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
async function fetchWithTimeout(url, options) {
|
|
4975
|
+
const controller = new AbortController();
|
|
4976
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
4977
|
+
try {
|
|
4978
|
+
return await fetch(url, {
|
|
4979
|
+
...options,
|
|
4980
|
+
signal: controller.signal
|
|
4981
|
+
});
|
|
4982
|
+
} catch (error) {
|
|
4983
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4984
|
+
throw new TransientTunnelValidationError(`Tunnel validation failed: timed out fetching ${url}.`);
|
|
4985
|
+
}
|
|
4986
|
+
throw new TransientTunnelValidationError(
|
|
4987
|
+
`Tunnel validation failed: could not fetch ${url}: ${errorMessage(error)}.`
|
|
4988
|
+
);
|
|
4989
|
+
} finally {
|
|
4990
|
+
clearTimeout(timeout);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async function retryTunnelValidation(validate, timeoutMs) {
|
|
4994
|
+
const startedAt = Date.now();
|
|
4995
|
+
let lastError;
|
|
4996
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
4997
|
+
try {
|
|
4998
|
+
await validate();
|
|
4999
|
+
return;
|
|
5000
|
+
} catch (error) {
|
|
5001
|
+
lastError = error;
|
|
5002
|
+
if (!(error instanceof TransientTunnelValidationError)) {
|
|
5003
|
+
throw error;
|
|
5004
|
+
}
|
|
5005
|
+
await delay(500);
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
throw lastError instanceof Error ? lastError : new Error("Tunnel validation failed before the public endpoint became reachable.");
|
|
5009
|
+
}
|
|
5010
|
+
var TransientTunnelValidationError = class extends Error {
|
|
5011
|
+
constructor(message) {
|
|
5012
|
+
super(message);
|
|
5013
|
+
this.name = "TransientTunnelValidationError";
|
|
5014
|
+
}
|
|
5015
|
+
};
|
|
5016
|
+
function isTransientHttpStatus(status) {
|
|
5017
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
5018
|
+
}
|
|
5019
|
+
function delay(ms) {
|
|
5020
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5021
|
+
}
|
|
5022
|
+
async function readValidationBody(response) {
|
|
5023
|
+
const text = await response.text();
|
|
5024
|
+
return text.slice(0, 2e4);
|
|
5025
|
+
}
|
|
5026
|
+
function assertNotHtml(response, body, url) {
|
|
5027
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5028
|
+
const trimmed = body.trimStart().toLowerCase();
|
|
5029
|
+
if (contentType.includes("text/html") || trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html")) {
|
|
5030
|
+
throw new Error([
|
|
5031
|
+
`Tunnel validation failed: ${url} returned HTML instead of MCP JSON.`,
|
|
5032
|
+
"This usually means the tunnel provider returned an interstitial, warning page, or bad route."
|
|
5033
|
+
].join("\n"));
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
function parseJsonObject(body, url) {
|
|
5037
|
+
try {
|
|
5038
|
+
const parsed = JSON.parse(body);
|
|
5039
|
+
if (isRecord3(parsed)) {
|
|
5040
|
+
return parsed;
|
|
5041
|
+
}
|
|
5042
|
+
} catch {
|
|
5043
|
+
}
|
|
5044
|
+
throw new Error(`Tunnel validation failed: ${url} did not return a JSON object.`);
|
|
5045
|
+
}
|
|
5046
|
+
function protectedResourceMetadataUrl(mcpUrl) {
|
|
5047
|
+
const url = new URL(mcpUrl.origin);
|
|
5048
|
+
const pathname = mcpUrl.pathname.replace(/\/+$/, "") || "/";
|
|
5049
|
+
url.pathname = pathname === "/" ? "/.well-known/oauth-protected-resource" : `/.well-known/oauth-protected-resource${pathname}`;
|
|
5050
|
+
return url.toString();
|
|
5051
|
+
}
|
|
5052
|
+
function tunnelOutputHint(output) {
|
|
5053
|
+
const trimmed = output.trim();
|
|
5054
|
+
return trimmed ? `Tunnel output:
|
|
5055
|
+
${trimmed}` : "";
|
|
5056
|
+
}
|
|
5057
|
+
function tail(value, maxLength) {
|
|
5058
|
+
return value.length > maxLength ? value.slice(value.length - maxLength) : value;
|
|
5059
|
+
}
|
|
5060
|
+
function errorMessage(error) {
|
|
5061
|
+
if (error instanceof Error) {
|
|
5062
|
+
const cause = error.cause;
|
|
5063
|
+
const causeMessage = cause instanceof Error ? ` (${cause.message})` : "";
|
|
5064
|
+
return `${error.message}${causeMessage}`;
|
|
5065
|
+
}
|
|
5066
|
+
return String(error);
|
|
5067
|
+
}
|
|
4182
5068
|
function hasCommand(command) {
|
|
4183
5069
|
return spawnSync("which", [command], { stdio: "ignore" }).status === 0;
|
|
4184
5070
|
}
|
|
@@ -4195,8 +5081,11 @@ function runInherited(command, args) {
|
|
|
4195
5081
|
});
|
|
4196
5082
|
});
|
|
4197
5083
|
}
|
|
4198
|
-
function appendPath(origin,
|
|
4199
|
-
return `${origin.replace(/\/+$/, "")}/${
|
|
5084
|
+
function appendPath(origin, path20) {
|
|
5085
|
+
return `${origin.replace(/\/+$/, "")}/${path20.replace(/^\/+/, "")}`;
|
|
5086
|
+
}
|
|
5087
|
+
function isRecord3(value) {
|
|
5088
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
4200
5089
|
}
|
|
4201
5090
|
|
|
4202
5091
|
// packages/cli/src/index.ts
|
|
@@ -4206,10 +5095,11 @@ async function main(argv) {
|
|
|
4206
5095
|
switch (command) {
|
|
4207
5096
|
case "build": {
|
|
4208
5097
|
const target = readTarget(argv);
|
|
5098
|
+
const host = readHost(argv);
|
|
4209
5099
|
const outDir = readOption(argv, "--out") ?? `out/${target}`;
|
|
4210
5100
|
const plugins = argv.includes("--plugins");
|
|
4211
5101
|
const strict = argv.includes("--strict");
|
|
4212
|
-
const manifest = await buildProject({ rootDir, outDir, plugins, strict, target });
|
|
5102
|
+
const manifest = await buildProject({ rootDir, host, outDir, plugins, strict, target });
|
|
4213
5103
|
printDiagnostics(manifest.diagnostics ?? []);
|
|
4214
5104
|
if (strict && manifest.diagnostics?.length) {
|
|
4215
5105
|
exit(1);
|
|
@@ -4217,6 +5107,12 @@ async function main(argv) {
|
|
|
4217
5107
|
console.log(
|
|
4218
5108
|
`Built ${manifest.tools.length} ${target} tool${manifest.tools.length === 1 ? "" : "s"}, ${manifest.resources.length} resource${manifest.resources.length === 1 ? "" : "s"}, and ${manifest.prompts.length} prompt${manifest.prompts.length === 1 ? "" : "s"} to ${outDir}.`
|
|
4219
5109
|
);
|
|
5110
|
+
console.log(`Host runtime: ${host}`);
|
|
5111
|
+
if (host === "vercel") {
|
|
5112
|
+
console.log(`Vercel MCP function: ${path19.join(outDir, VERCEL_ENTRYPOINT)}`);
|
|
5113
|
+
} else {
|
|
5114
|
+
console.log(`Hostable MCP server: ${path19.join(outDir, SERVER_ENTRYPOINT)}`);
|
|
5115
|
+
}
|
|
4220
5116
|
if (plugins) {
|
|
4221
5117
|
console.log("Built claude-plugin package.");
|
|
4222
5118
|
}
|
|
@@ -4248,18 +5144,20 @@ async function main(argv) {
|
|
|
4248
5144
|
const target = readTarget(argv);
|
|
4249
5145
|
const tunnelProvider = readTunnelProvider(argv);
|
|
4250
5146
|
const outDir = `.sidecar/dev/${target}`;
|
|
5147
|
+
const loadedAuth = await loadRuntimeAuth(rootDir);
|
|
5148
|
+
const runtimeConfig = await loadRuntimeConfig(rootDir);
|
|
5149
|
+
const proxy = await loadRuntimeProxy(rootDir);
|
|
5150
|
+
const runtimeToolEntries = await analyzeProjectTools(rootDir, { target });
|
|
5151
|
+
const runtimeTools = await loadRuntimeTools(rootDir, runtimeToolEntries);
|
|
4251
5152
|
const manifest = await buildProject({ rootDir, outDir, target });
|
|
4252
5153
|
printDiagnostics(manifest.diagnostics ?? []);
|
|
4253
|
-
const tools =
|
|
5154
|
+
const tools = attachBuiltToolDescriptors(runtimeTools, manifest);
|
|
4254
5155
|
let tunnel;
|
|
4255
5156
|
if (tunnelProvider) {
|
|
4256
5157
|
tunnel = await startTunnel({ provider: tunnelProvider, port, path: "/mcp" });
|
|
4257
5158
|
process.env.SIDECAR_MCP_URL = tunnel.mcpUrl;
|
|
4258
5159
|
}
|
|
4259
|
-
const loadedAuth = await loadRuntimeAuth(rootDir);
|
|
4260
|
-
const runtimeConfig = await loadRuntimeConfig(rootDir);
|
|
4261
5160
|
const auth = loadedAuth && tunnel ? loadedAuth.withResource(tunnel.mcpUrl) : loadedAuth;
|
|
4262
|
-
const proxy = await loadRuntimeProxy(rootDir);
|
|
4263
5161
|
const resources = await loadResources(rootDir, outDir, manifest);
|
|
4264
5162
|
const prompts = await loadRuntimePrompts(rootDir, manifest);
|
|
4265
5163
|
const server = createSidecarHttpServer({
|
|
@@ -4280,14 +5178,29 @@ async function main(argv) {
|
|
|
4280
5178
|
pageSize: manifest.config.pagination.pageSize
|
|
4281
5179
|
}
|
|
4282
5180
|
});
|
|
4283
|
-
|
|
5181
|
+
let listening = false;
|
|
5182
|
+
try {
|
|
5183
|
+
await listenOnLocalhost(server, port);
|
|
5184
|
+
listening = true;
|
|
5185
|
+
if (tunnel) {
|
|
5186
|
+
await validateTunnelEndpoint({
|
|
5187
|
+
mcpUrl: tunnel.mcpUrl,
|
|
5188
|
+
auth: Boolean(auth)
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
4284
5191
|
console.log(`MCP running on Streamable HTTP (${target}) at http://127.0.0.1:${port}/mcp`);
|
|
4285
5192
|
console.log(`Loaded ${tools.length} tool${tools.length === 1 ? "" : "s"}, ${resources.length} resource${resources.length === 1 ? "" : "s"}, and ${prompts.length} prompt${prompts.length === 1 ? "" : "s"}.`);
|
|
4286
5193
|
if (tunnel) {
|
|
4287
|
-
console.log(`HTTPS tunnel (${tunnel.provider})
|
|
5194
|
+
console.log(`HTTPS tunnel (${tunnel.provider}) validated: ${tunnel.mcpUrl}`);
|
|
4288
5195
|
console.log("Use this HTTPS MCP URL in ChatGPT, Claude, or a Claude plugin install.");
|
|
4289
5196
|
}
|
|
4290
|
-
})
|
|
5197
|
+
} catch (error) {
|
|
5198
|
+
tunnel?.close();
|
|
5199
|
+
if (listening) {
|
|
5200
|
+
await closeServer(server);
|
|
5201
|
+
}
|
|
5202
|
+
throw error;
|
|
5203
|
+
}
|
|
4291
5204
|
const shutdown = () => {
|
|
4292
5205
|
tunnel?.close();
|
|
4293
5206
|
server.close(() => exit(0));
|
|
@@ -4330,7 +5243,7 @@ async function main(argv) {
|
|
|
4330
5243
|
console.log(`Sidecar
|
|
4331
5244
|
|
|
4332
5245
|
Usage:
|
|
4333
|
-
sidecar build [--cwd <dir>] [--target mcp|chatgpt|claude] [--out <dir>] [--plugins] [--strict]
|
|
5246
|
+
sidecar build [--cwd <dir>] [--target mcp|chatgpt|claude] [--host node|vercel] [--out <dir>] [--plugins] [--strict]
|
|
4334
5247
|
sidecar check [--cwd <dir>] [--target mcp|chatgpt|claude] [--strict]
|
|
4335
5248
|
sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <port>] [--tunnel [cloudflared|wrangler]]
|
|
4336
5249
|
sidecar inspect [--cwd <dir>] [--target mcp|chatgpt|claude]
|
|
@@ -4345,9 +5258,16 @@ function readTarget(argv) {
|
|
|
4345
5258
|
}
|
|
4346
5259
|
throw new Error(`Unsupported Sidecar target "${target}". Expected mcp, chatgpt, or claude.`);
|
|
4347
5260
|
}
|
|
5261
|
+
function readHost(argv) {
|
|
5262
|
+
const host = readOption(argv, "--host") ?? "node";
|
|
5263
|
+
if (host === "node" || host === "vercel") {
|
|
5264
|
+
return host;
|
|
5265
|
+
}
|
|
5266
|
+
throw new Error(`Unsupported Sidecar host "${host}". Expected node or vercel.`);
|
|
5267
|
+
}
|
|
4348
5268
|
async function previewComponents(options) {
|
|
4349
|
-
const cliDir =
|
|
4350
|
-
const css = await readFile8(
|
|
5269
|
+
const cliDir = path19.dirname(fileURLToPath(import.meta.url));
|
|
5270
|
+
const css = await readFile8(path19.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path19.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
|
|
4351
5271
|
const html = renderComponentPreviewHtml(
|
|
4352
5272
|
options.host,
|
|
4353
5273
|
options.compare,
|
|
@@ -4387,10 +5307,10 @@ async function previewComponents(options) {
|
|
|
4387
5307
|
server.close();
|
|
4388
5308
|
}
|
|
4389
5309
|
async function writeComponentApproval(rootDir, approval) {
|
|
4390
|
-
const dir =
|
|
4391
|
-
await
|
|
4392
|
-
await
|
|
4393
|
-
|
|
5310
|
+
const dir = path19.join(rootDir, ".sidecar", "approvals");
|
|
5311
|
+
await mkdir10(dir, { recursive: true });
|
|
5312
|
+
await writeFile10(
|
|
5313
|
+
path19.join(dir, `components.${approval.host}.json`),
|
|
4394
5314
|
`${JSON.stringify(approval, null, 2)}
|
|
4395
5315
|
`
|
|
4396
5316
|
);
|
|
@@ -4742,84 +5662,98 @@ function printDiagnostics(diagnostics) {
|
|
|
4742
5662
|
}
|
|
4743
5663
|
}
|
|
4744
5664
|
async function loadRuntimeAuth(rootDir) {
|
|
4745
|
-
const authPath =
|
|
4746
|
-
if (!
|
|
5665
|
+
const authPath = path19.join(rootDir, "auth.ts");
|
|
5666
|
+
if (!existsSync6(authPath)) {
|
|
4747
5667
|
return void 0;
|
|
4748
5668
|
}
|
|
4749
|
-
const parentURL = pathToFileURL(
|
|
4750
|
-
const tsconfigPath =
|
|
4751
|
-
const tsconfig =
|
|
5669
|
+
const parentURL = pathToFileURL(path19.join(rootDir, "sidecar.config.ts")).href;
|
|
5670
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5671
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4752
5672
|
const module = await tsImport(pathToFileURL(authPath).href, {
|
|
4753
5673
|
parentURL,
|
|
4754
5674
|
tsconfig
|
|
4755
5675
|
});
|
|
4756
|
-
|
|
5676
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5677
|
+
if (!isSidecarAuth(defaultExport)) {
|
|
4757
5678
|
throw new Error("auth.ts must default-export auth({ ... }) from sidecar-ai.");
|
|
4758
5679
|
}
|
|
4759
|
-
return
|
|
5680
|
+
return defaultExport;
|
|
4760
5681
|
}
|
|
4761
5682
|
async function loadRuntimeProxy(rootDir) {
|
|
4762
|
-
const proxyPath =
|
|
4763
|
-
if (!
|
|
5683
|
+
const proxyPath = path19.join(rootDir, "proxy.ts");
|
|
5684
|
+
if (!existsSync6(proxyPath)) {
|
|
4764
5685
|
return void 0;
|
|
4765
5686
|
}
|
|
4766
|
-
const parentURL = pathToFileURL(
|
|
4767
|
-
const tsconfigPath =
|
|
4768
|
-
const tsconfig =
|
|
5687
|
+
const parentURL = pathToFileURL(path19.join(rootDir, "sidecar.config.ts")).href;
|
|
5688
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5689
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4769
5690
|
const module = await tsImport(pathToFileURL(proxyPath).href, {
|
|
4770
5691
|
parentURL,
|
|
4771
5692
|
tsconfig
|
|
4772
5693
|
});
|
|
4773
|
-
|
|
5694
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5695
|
+
if (!isSidecarProxy(defaultExport)) {
|
|
4774
5696
|
throw new Error("proxy.ts must default-export proxy({ ... }) from @sidecar-ai/server/proxy.");
|
|
4775
5697
|
}
|
|
4776
|
-
return
|
|
5698
|
+
return defaultExport;
|
|
4777
5699
|
}
|
|
4778
5700
|
async function loadRuntimeConfig(rootDir) {
|
|
4779
|
-
const configPath =
|
|
4780
|
-
if (!
|
|
5701
|
+
const configPath = path19.join(rootDir, "sidecar.config.ts");
|
|
5702
|
+
if (!existsSync6(configPath)) {
|
|
4781
5703
|
return void 0;
|
|
4782
5704
|
}
|
|
4783
5705
|
const parentURL = pathToFileURL(configPath).href;
|
|
4784
|
-
const tsconfigPath =
|
|
4785
|
-
const tsconfig =
|
|
5706
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5707
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4786
5708
|
const module = await tsImport(pathToFileURL(configPath).href, {
|
|
4787
5709
|
parentURL,
|
|
4788
5710
|
tsconfig
|
|
4789
5711
|
});
|
|
4790
|
-
|
|
5712
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5713
|
+
if (!defaultExport || typeof defaultExport !== "object") {
|
|
4791
5714
|
throw new Error("sidecar.config.ts must default-export defineConfig({ ... }) from sidecar-ai.");
|
|
4792
5715
|
}
|
|
4793
|
-
return
|
|
5716
|
+
return defaultExport;
|
|
4794
5717
|
}
|
|
4795
|
-
async function loadRuntimeTools(rootDir,
|
|
4796
|
-
const parentURL = pathToFileURL(
|
|
4797
|
-
const tsconfigPath =
|
|
4798
|
-
const tsconfig =
|
|
5718
|
+
async function loadRuntimeTools(rootDir, entries) {
|
|
5719
|
+
const parentURL = pathToFileURL(path19.join(rootDir, "sidecar.config.ts")).href;
|
|
5720
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5721
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4799
5722
|
const loaded = [];
|
|
4800
|
-
for (const entry of
|
|
4801
|
-
const sourcePath =
|
|
5723
|
+
for (const entry of entries) {
|
|
5724
|
+
const sourcePath = path19.join(rootDir, entry.sourceFile);
|
|
4802
5725
|
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4803
5726
|
parentURL,
|
|
4804
5727
|
tsconfig
|
|
4805
5728
|
});
|
|
4806
|
-
|
|
5729
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5730
|
+
if (!isSidecarTool(defaultExport)) {
|
|
4807
5731
|
throw new Error(`${entry.sourceFile} did not default-export a Sidecar tool.`);
|
|
4808
5732
|
}
|
|
4809
5733
|
loaded.push({
|
|
4810
|
-
|
|
5734
|
+
sourceFile: entry.sourceFile,
|
|
5735
|
+
tool: defaultExport,
|
|
4811
5736
|
descriptor: entry.descriptor
|
|
4812
5737
|
});
|
|
4813
5738
|
}
|
|
4814
5739
|
return loaded;
|
|
4815
5740
|
}
|
|
5741
|
+
function attachBuiltToolDescriptors(loadedTools, manifest) {
|
|
5742
|
+
const descriptorsBySource = new Map(
|
|
5743
|
+
manifest.tools.map((entry) => [entry.sourceFile, entry.descriptor])
|
|
5744
|
+
);
|
|
5745
|
+
return loadedTools.map(({ sourceFile, tool, descriptor }) => ({
|
|
5746
|
+
tool,
|
|
5747
|
+
descriptor: descriptorsBySource.get(sourceFile) ?? descriptor
|
|
5748
|
+
}));
|
|
5749
|
+
}
|
|
4816
5750
|
async function loadResources(rootDir, outDir, manifest) {
|
|
4817
5751
|
const resources = [];
|
|
4818
5752
|
for (const entry of manifest.tools) {
|
|
4819
5753
|
if (!entry.widget?.outputFile) {
|
|
4820
5754
|
continue;
|
|
4821
5755
|
}
|
|
4822
|
-
const text = await readFile8(
|
|
5756
|
+
const text = await readFile8(path19.join(rootDir, outDir, entry.widget.outputFile), "utf8");
|
|
4823
5757
|
resources.push({
|
|
4824
5758
|
uri: entry.widget.resourceUri,
|
|
4825
5759
|
name: entry.name,
|
|
@@ -4829,47 +5763,72 @@ async function loadResources(rootDir, outDir, manifest) {
|
|
|
4829
5763
|
_meta: entry.widget.resourceMeta
|
|
4830
5764
|
});
|
|
4831
5765
|
}
|
|
4832
|
-
const parentURL = pathToFileURL(
|
|
4833
|
-
const tsconfigPath =
|
|
4834
|
-
const tsconfig =
|
|
5766
|
+
const parentURL = pathToFileURL(path19.join(rootDir, "sidecar.config.ts")).href;
|
|
5767
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5768
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4835
5769
|
for (const entry of manifest.resources) {
|
|
4836
|
-
const sourcePath =
|
|
5770
|
+
const sourcePath = path19.join(rootDir, entry.sourceFile);
|
|
4837
5771
|
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4838
5772
|
parentURL,
|
|
4839
5773
|
tsconfig
|
|
4840
5774
|
});
|
|
4841
|
-
|
|
5775
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5776
|
+
if (!isSidecarResource(defaultExport)) {
|
|
4842
5777
|
throw new Error(`${entry.sourceFile} did not default-export a Sidecar resource.`);
|
|
4843
5778
|
}
|
|
4844
5779
|
resources.push({
|
|
4845
5780
|
uri: entry.uri,
|
|
4846
5781
|
descriptor: entry.descriptor,
|
|
4847
|
-
resource:
|
|
5782
|
+
resource: defaultExport
|
|
4848
5783
|
});
|
|
4849
5784
|
}
|
|
4850
5785
|
return resources;
|
|
4851
5786
|
}
|
|
4852
5787
|
async function loadRuntimePrompts(rootDir, manifest) {
|
|
4853
|
-
const parentURL = pathToFileURL(
|
|
4854
|
-
const tsconfigPath =
|
|
4855
|
-
const tsconfig =
|
|
5788
|
+
const parentURL = pathToFileURL(path19.join(rootDir, "sidecar.config.ts")).href;
|
|
5789
|
+
const tsconfigPath = path19.join(rootDir, "tsconfig.json");
|
|
5790
|
+
const tsconfig = existsSync6(tsconfigPath) ? tsconfigPath : false;
|
|
4856
5791
|
const loaded = [];
|
|
4857
5792
|
for (const entry of manifest.prompts) {
|
|
4858
|
-
const sourcePath =
|
|
5793
|
+
const sourcePath = path19.join(rootDir, entry.sourceFile);
|
|
4859
5794
|
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4860
5795
|
parentURL,
|
|
4861
5796
|
tsconfig
|
|
4862
5797
|
});
|
|
4863
|
-
|
|
5798
|
+
const defaultExport = unwrapRuntimeDefault(module.default);
|
|
5799
|
+
if (!isSidecarPrompt(defaultExport)) {
|
|
4864
5800
|
throw new Error(`${entry.sourceFile} did not default-export a Sidecar prompt.`);
|
|
4865
5801
|
}
|
|
4866
5802
|
loaded.push({
|
|
4867
|
-
prompt:
|
|
5803
|
+
prompt: defaultExport,
|
|
4868
5804
|
descriptor: entry.descriptor
|
|
4869
5805
|
});
|
|
4870
5806
|
}
|
|
4871
5807
|
return loaded;
|
|
4872
5808
|
}
|
|
5809
|
+
function unwrapRuntimeDefault(value) {
|
|
5810
|
+
if (value && typeof value === "object" && "default" in value && Object.keys(value).length === 1) {
|
|
5811
|
+
return unwrapRuntimeDefault(value.default);
|
|
5812
|
+
}
|
|
5813
|
+
return value;
|
|
5814
|
+
}
|
|
5815
|
+
function listenOnLocalhost(server, port) {
|
|
5816
|
+
return new Promise((resolve, reject) => {
|
|
5817
|
+
const onError = (error) => {
|
|
5818
|
+
reject(error);
|
|
5819
|
+
};
|
|
5820
|
+
server.once("error", onError);
|
|
5821
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5822
|
+
server.off("error", onError);
|
|
5823
|
+
resolve();
|
|
5824
|
+
});
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5827
|
+
function closeServer(server) {
|
|
5828
|
+
return new Promise((resolve) => {
|
|
5829
|
+
server.close(() => resolve());
|
|
5830
|
+
});
|
|
5831
|
+
}
|
|
4873
5832
|
function readOption(argv, name) {
|
|
4874
5833
|
const index = argv.indexOf(name);
|
|
4875
5834
|
if (index === -1) {
|