@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/index.ts
4
- import { existsSync as existsSync5, realpathSync } from "fs";
5
- import { mkdir as mkdir9, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
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 path18 from "path";
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(value && typeof value === "object" && value[toolBrand]);
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(value && typeof value === "object" && value[resourceBrand]);
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(value && typeof value === "object" && value[promptBrand]);
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.csp?.resourceDomains ? [...options.csp.resourceDomains] : [],
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 mkdir8, writeFile as writeFile8 } from "fs/promises";
1640
- import path17 from "path";
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 printed HTTPS MCP URL.
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 = path17.resolve(options.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 mkdir8(outDir, { recursive: true });
2990
- await writeFile8(
2991
- path17.join(outDir, "manifest.sidecar.json"),
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 writeFile8(path17.join(outDir, "README.md"), renderMcpReadme(manifest));
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, path17.dirname(outDir), manifest);
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 = path17.resolve(rootDir, output);
3004
- const relative = path17.relative(rootDir, resolved);
3005
- if (relative.startsWith("..") || path17.isAbsolute(relative)) {
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 an HTTPS MCP URL that can be added to ChatGPT, Claude, or a Claude plugin install.
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/index.ts
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: JSONRPC_VERSION,
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: JSONRPC_VERSION,
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
- const args = loaded.descriptorProvided ? validateAgainstSchema(
3269
- loaded.descriptor.inputSchema,
3270
- params?.arguments ?? {},
3271
- `Invalid parameters for tool "${name}".`
3272
- ) : params?.arguments ?? {};
3273
- const result = await withTimeout(
3274
- executeTool(loaded.tool, args, ctx),
3275
- this.options.toolTimeoutMs,
3276
- controller
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(_request) {
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
- const mcp = createSidecarMcpServer(options);
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
- return createServer(async (request, response) => {
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
- response.writeHead(405, { "content-type": "application/json" });
3423
- response.end(JSON.stringify({ error: "sse_not_supported" }));
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" || pathname !== endpoint) {
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: JSONRPC_VERSION,
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: JSONRPC_VERSION,
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
- return promise;
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
- controller.abort();
3739
- reject(new JsonRpcError(-32e3, "Tool execution timed out."));
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, path19) {
3760
- if (schema.anyOf?.length && !schema.anyOf.some((entry) => !schemaFailure(entry, value, path19))) {
3761
- return `${path19} must match one anyOf schema.`;
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, path19)).length !== 1) {
3764
- return `${path19} must match exactly one oneOf schema.`;
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, path19);
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 `${path19} must equal ${JSON.stringify(schema.const)}.`;
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 `${path19} must be one of the declared enum values.`;
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 `${path19} must be ${types.join(" or ")}.`;
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 `${path19} must be an object.`;
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 `${path19}.${required} is required.`;
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], `${path19}.${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 `${path19}.${extra} is not allowed.`;
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 `${path19} must be an array.`;
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, `${path19}[${index}]`);
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 `${path19} is shorter than ${schema.minLength}.`;
4484
+ return `${path20} is shorter than ${schema.minLength}.`;
3822
4485
  }
3823
4486
  if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
3824
- return `${path19} is longer than ${schema.maxLength}.`;
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 `${path19} is less than ${schema.minimum}.`;
4492
+ return `${path20} is less than ${schema.minimum}.`;
3830
4493
  }
3831
4494
  if (schema.maximum !== void 0 && value > schema.maximum) {
3832
- return `${path19} is greater than ${schema.maximum}.`;
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 !== JSONRPC_VERSION || typeof record.method !== "string") {
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: JSONRPC_VERSION,
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 !== JSONRPC_VERSION || !("id" in record) || !("result" in record || "error" in record)) {
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 path19 = options.path ?? "/mcp";
4673
+ const path20 = options.path ?? "/mcp";
4011
4674
  if (provider === "cloudflared") {
4012
- return startCloudflared(localUrl, path19, options.timeoutMs);
4013
- }
4014
- return startWrangler(localUrl, path19, options.timeoutMs);
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, path19, timeoutMs = 2e4) {
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, path19),
4809
+ mcpUrl: appendPath(publicUrl, path20),
4127
4810
  close() {
4128
4811
  child.kill("SIGTERM");
4129
4812
  }
4130
4813
  };
4131
4814
  }
4132
- async function startWrangler(localUrl, path19, timeoutMs = 2e4) {
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, path19),
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("Timed out waiting for HTTPS tunnel URL."));
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
- const match = chunk.toString("utf8").match(pattern);
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(`Tunnel process exited before producing a URL${code === null ? "" : ` with code ${code}`}.`));
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, path19) {
4199
- return `${origin.replace(/\/+$/, "")}/${path19.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 = await loadRuntimeTools(rootDir, manifest);
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
- server.listen(port, "127.0.0.1", () => {
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}) ready: ${tunnel.mcpUrl}`);
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 = path18.dirname(fileURLToPath(import.meta.url));
4350
- const css = await readFile8(path18.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path18.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
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 = path18.join(rootDir, ".sidecar", "approvals");
4391
- await mkdir9(dir, { recursive: true });
4392
- await writeFile9(
4393
- path18.join(dir, `components.${approval.host}.json`),
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 = path18.join(rootDir, "auth.ts");
4746
- if (!existsSync5(authPath)) {
5665
+ const authPath = path19.join(rootDir, "auth.ts");
5666
+ if (!existsSync6(authPath)) {
4747
5667
  return void 0;
4748
5668
  }
4749
- const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
4750
- const tsconfigPath = path18.join(rootDir, "tsconfig.json");
4751
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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
- if (!isSidecarAuth(module.default)) {
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 module.default;
5680
+ return defaultExport;
4760
5681
  }
4761
5682
  async function loadRuntimeProxy(rootDir) {
4762
- const proxyPath = path18.join(rootDir, "proxy.ts");
4763
- if (!existsSync5(proxyPath)) {
5683
+ const proxyPath = path19.join(rootDir, "proxy.ts");
5684
+ if (!existsSync6(proxyPath)) {
4764
5685
  return void 0;
4765
5686
  }
4766
- const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
4767
- const tsconfigPath = path18.join(rootDir, "tsconfig.json");
4768
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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
- if (!isSidecarProxy(module.default)) {
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 module.default;
5698
+ return defaultExport;
4777
5699
  }
4778
5700
  async function loadRuntimeConfig(rootDir) {
4779
- const configPath = path18.join(rootDir, "sidecar.config.ts");
4780
- if (!existsSync5(configPath)) {
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 = path18.join(rootDir, "tsconfig.json");
4785
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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
- if (!module.default || typeof module.default !== "object") {
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 module.default;
5716
+ return defaultExport;
4794
5717
  }
4795
- async function loadRuntimeTools(rootDir, manifest) {
4796
- const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
4797
- const tsconfigPath = path18.join(rootDir, "tsconfig.json");
4798
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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 manifest.tools) {
4801
- const sourcePath = path18.join(rootDir, entry.sourceFile);
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
- if (!isSidecarTool(module.default)) {
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
- tool: module.default,
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(path18.join(rootDir, outDir, entry.widget.outputFile), "utf8");
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(path18.join(rootDir, "sidecar.config.ts")).href;
4833
- const tsconfigPath = path18.join(rootDir, "tsconfig.json");
4834
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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 = path18.join(rootDir, entry.sourceFile);
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
- if (!isSidecarResource(module.default)) {
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: module.default
5782
+ resource: defaultExport
4848
5783
  });
4849
5784
  }
4850
5785
  return resources;
4851
5786
  }
4852
5787
  async function loadRuntimePrompts(rootDir, manifest) {
4853
- const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
4854
- const tsconfigPath = path18.join(rootDir, "tsconfig.json");
4855
- const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
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 = path18.join(rootDir, entry.sourceFile);
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
- if (!isSidecarPrompt(module.default)) {
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: module.default,
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) {