@sidecar-ai/cli 0.1.0-alpha.10 → 0.1.0-alpha.11

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,45 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/index.ts
4
- import { existsSync as existsSync7, realpathSync } from "fs";
4
+ import { existsSync as existsSync8, realpathSync } from "fs";
5
5
  import { mkdir as mkdir10, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
6
- import { createServer as createServer2 } from "http";
7
- import path20 from "path";
6
+ import { spawn as spawn2 } from "child_process";
7
+ import { createServer as createServer3 } from "http";
8
+ import path21 from "path";
8
9
  import { createInterface as createInterface2 } from "readline/promises";
9
- import { fileURLToPath, pathToFileURL as pathToFileURL3 } from "url";
10
- import { cwd, exit, stdin as stdin2, stdout as stdout2 } from "process";
10
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "url";
11
+ import { cwd, execPath, exit, stdin as stdin2, stdout as stdout2 } from "process";
11
12
  import { tsImport as tsImport2 } from "tsx/esm/api";
12
13
 
13
- // packages/auth/src/core.ts
14
- var authBrand = /* @__PURE__ */ Symbol.for("sidecar.auth");
15
- function isSidecarAuth(value) {
16
- return Boolean(
17
- value && typeof value === "object" && (value[authBrand] || value.kind === "sidecar.auth")
18
- );
19
- }
20
-
21
14
  // packages/core/src/index.ts
22
- var MCP_APP_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
23
15
  var toolBrand = /* @__PURE__ */ Symbol.for("sidecar.tool");
24
16
  var toolResultBrand = /* @__PURE__ */ Symbol.for("sidecar.toolResult");
25
- var resourceBrand = /* @__PURE__ */ Symbol.for("sidecar.resource");
26
17
  var resourceResultBrand = /* @__PURE__ */ Symbol.for("sidecar.resourceResult");
27
- var promptBrand = /* @__PURE__ */ Symbol.for("sidecar.prompt");
28
18
  function isSidecarTool(value) {
29
19
  return Boolean(
30
20
  value && typeof value === "object" && (value[toolBrand] || value.kind === "sidecar.tool")
31
21
  );
32
22
  }
33
- function isSidecarResource(value) {
34
- return Boolean(
35
- value && typeof value === "object" && (value[resourceBrand] || value.kind === "sidecar.resource")
36
- );
37
- }
38
- function isSidecarPrompt(value) {
39
- return Boolean(
40
- value && typeof value === "object" && (value[promptBrand] || value.kind === "sidecar.prompt")
41
- );
42
- }
43
23
  var toolResult = Object.assign(
44
24
  createToolResult,
45
25
  {
@@ -94,57 +74,6 @@ function createResourceResult(input) {
94
74
  });
95
75
  return resultEnvelope;
96
76
  }
97
- function isToolResult(value) {
98
- return Boolean(
99
- value && typeof value === "object" && value[toolResultBrand] === true
100
- );
101
- }
102
- function isResourceResult(value) {
103
- return Boolean(
104
- value && typeof value === "object" && value[resourceResultBrand] === true
105
- );
106
- }
107
- function normalizeToolResult(value) {
108
- if (!isToolResult(value)) {
109
- throw new SidecarRuntimeError(
110
- "Tool execute() must return toolResult({ structuredContent, content, meta }).",
111
- "invalid_tool_result"
112
- );
113
- }
114
- return stripUndefined({
115
- structuredContent: stripJsonUndefined(value.structuredContent),
116
- content: value.content ?? [],
117
- _meta: stripJsonUndefined(value._meta),
118
- isError: value.isError
119
- });
120
- }
121
- function normalizeResourceResult(value, uri, defaultMimeType) {
122
- if (!isResourceResult(value)) {
123
- throw new SidecarRuntimeError(
124
- "Resource read() must return resourceResult(...).",
125
- "invalid_resource_result"
126
- );
127
- }
128
- return {
129
- contents: value.contents.map(
130
- (content) => normalizeResourceContent(content, uri, defaultMimeType)
131
- )
132
- };
133
- }
134
- async function executeTool(sidecarTool, params, ctx) {
135
- const parsedParams = validateParams(sidecarTool, params);
136
- const value = await sidecarTool.execute(parsedParams, ctx);
137
- return normalizeToolResult(value);
138
- }
139
- async function executeResource(sidecarResource, ctx, options) {
140
- const value = await sidecarResource.read(ctx);
141
- return normalizeResourceResult(value, options.uri, sidecarResource.mimeType ?? options.mimeType);
142
- }
143
- async function executePrompt(sidecarPrompt, args, ctx) {
144
- const parsedArgs = validatePromptArgs(sidecarPrompt, args);
145
- const value = await sidecarPrompt.run(parsedArgs, ctx);
146
- return normalizePromptResult(value, sidecarPrompt.description);
147
- }
148
77
  function createToolDescriptor(definition) {
149
78
  const machineName = definition.id ?? toMachineName(definition.name);
150
79
  validateToolId(machineName);
@@ -309,16 +238,6 @@ var SidecarRuntimeError = class extends Error {
309
238
  }
310
239
  code;
311
240
  };
312
- function validateParams(sidecarTool, params) {
313
- if (!sidecarTool.params) {
314
- return params ?? {};
315
- }
316
- const parsed = sidecarTool.params.safeParse(params ?? {});
317
- if (parsed.success) {
318
- return parsed.data;
319
- }
320
- throw new SidecarRuntimeError(`Invalid parameters for tool "${sidecarTool.name}".`, "invalid_tool_params");
321
- }
322
241
  function normalizeRequiredContent(content) {
323
242
  if (typeof content === "string") {
324
243
  return [toolResult.text(content)];
@@ -332,49 +251,6 @@ function normalizeRequiredContent(content) {
332
251
  "invalid_tool_result"
333
252
  );
334
253
  }
335
- function normalizeResourceContent(content, uri, defaultMimeType) {
336
- const annotations = normalizeAnnotations(content.annotations);
337
- const meta = content.meta;
338
- if (isBinaryLike(content.content)) {
339
- return stripUndefined({
340
- uri,
341
- mimeType: content.mimeType ?? defaultMimeType ?? "application/octet-stream",
342
- blob: bytesToBase64(content.content),
343
- annotations,
344
- _meta: meta
345
- });
346
- }
347
- if (typeof content.content === "string") {
348
- return stripUndefined({
349
- uri,
350
- mimeType: content.mimeType ?? defaultMimeType ?? "text/plain",
351
- text: content.content,
352
- annotations,
353
- _meta: meta
354
- });
355
- }
356
- return stripUndefined({
357
- uri,
358
- mimeType: content.mimeType ?? defaultMimeType ?? "application/json",
359
- text: JSON.stringify(content.content),
360
- annotations,
361
- _meta: meta
362
- });
363
- }
364
- function isBinaryLike(value) {
365
- return value instanceof ArrayBuffer || value instanceof Uint8Array;
366
- }
367
- function bytesToBase64(value) {
368
- const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value;
369
- if (typeof Buffer !== "undefined") {
370
- return Buffer.from(bytes).toString("base64");
371
- }
372
- let binary = "";
373
- for (const byte of bytes) {
374
- binary += String.fromCharCode(byte);
375
- }
376
- return btoa(binary);
377
- }
378
254
  function normalizeAnnotations(annotations) {
379
255
  if (!annotations) {
380
256
  return void 0;
@@ -411,45 +287,6 @@ function promptArguments(args) {
411
287
  function isReadonlyArray(value) {
412
288
  return Array.isArray(value);
413
289
  }
414
- function validatePromptArgs(promptDefinition, args) {
415
- const input = args && typeof args === "object" && !Array.isArray(args) ? args : {};
416
- for (const argument of promptArguments(promptDefinition.args) ?? []) {
417
- if (argument.required !== false && !(argument.name in input)) {
418
- throw new SidecarRuntimeError(
419
- `Prompt "${promptDefinition.name ?? promptDefinition.title}" is missing required argument "${argument.name}".`,
420
- "invalid_prompt_args"
421
- );
422
- }
423
- }
424
- return input;
425
- }
426
- function normalizePromptResult(value, defaultDescription) {
427
- if (typeof value === "string") {
428
- return {
429
- description: defaultDescription,
430
- messages: [{
431
- role: "user",
432
- content: toolResult.text(value)
433
- }]
434
- };
435
- }
436
- if (Array.isArray(value)) {
437
- return {
438
- description: defaultDescription,
439
- messages: value
440
- };
441
- }
442
- if ("messages" in value) {
443
- return {
444
- description: value.description ?? defaultDescription,
445
- messages: value.messages
446
- };
447
- }
448
- return {
449
- description: defaultDescription,
450
- messages: [value]
451
- };
452
- }
453
290
  function stripUndefined(value) {
454
291
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
455
292
  }
@@ -991,6 +828,83 @@ createRoot(document.getElementById("root")!).render(
991
828
  entry.descriptor._meta = mergeWidgetMeta(entry.descriptor._meta, widgetMeta(resourceUri, entry.widget.options, entry.target));
992
829
  }
993
830
  }
831
+ async function buildCodeModeWidget(rootDir, outDir, tools, target, config = void 0) {
832
+ const renderable = tools.filter(
833
+ (entry) => Boolean(entry.widget)
834
+ );
835
+ if (!renderable.length) {
836
+ return void 0;
837
+ }
838
+ const cacheDir = path4.join(rootDir, ".sidecar", "cache", "widgets");
839
+ await mkdir(cacheDir, { recursive: true });
840
+ const appStyle = await prepareAppStyle(rootDir, cacheDir);
841
+ const configure = await loadWidgetBundlerHook(rootDir, cacheDir, config?.configure);
842
+ const safeId = "code-mode";
843
+ const entryFile = path4.join(cacheDir, `${safeId}.entry.tsx`);
844
+ await writeFile(entryFile, renderCodeModeWidgetEntry(rootDir, entryFile, renderable, appStyle));
845
+ const baseOptions = enforceWidgetBuildInvariants({
846
+ absWorkingDir: rootDir,
847
+ alias: sidecarWidgetAliases(rootDir),
848
+ bundle: true,
849
+ define: {
850
+ "process.env.NODE_ENV": JSON.stringify("production")
851
+ },
852
+ entryPoints: [entryFile],
853
+ format: "iife",
854
+ jsx: "automatic",
855
+ minify: true,
856
+ nodePaths: [
857
+ path4.join(rootDir, "node_modules"),
858
+ path4.join(process.cwd(), "node_modules")
859
+ ],
860
+ outfile: "widget.js",
861
+ platform: "browser",
862
+ sourcemap: false,
863
+ write: false
864
+ });
865
+ const staticOptions = widgetBuildOptionsFromConfig(rootDir, config);
866
+ const configuredOptions = configure ? await configureWidgetBuild(configure, {
867
+ rootDir,
868
+ outDir,
869
+ entryFile,
870
+ widget: {
871
+ toolId: "execute_code",
872
+ toolName: "Execute Code",
873
+ target,
874
+ sourceFile: path4.relative(rootDir, entryFile)
875
+ },
876
+ esbuildOptions: mergeWidgetBuildOptions(baseOptions, staticOptions)
877
+ }) : void 0;
878
+ const bundled = await esbuild(enforceWidgetBuildInvariants(
879
+ mergeWidgetBuildOptions(baseOptions, staticOptions, configuredOptions),
880
+ { rootDir, entryFile }
881
+ ));
882
+ const outputFiles = bundled.outputFiles ?? [];
883
+ const javascript = outputFiles.find((file) => file.path.endsWith(".js"))?.text ?? "";
884
+ const css = outputFiles.filter((file) => file.path.endsWith(".css")).map((file) => file.text).join("\n");
885
+ const html = renderWidgetHtml("Execute Code", javascript, css);
886
+ const hash = createHash("sha256").update(html).digest("hex").slice(0, 12);
887
+ const outputDir = path4.join(outDir, "public", "widgets", safeId);
888
+ const outputFile = path4.join(outputDir, `widget.${hash}.html`);
889
+ const resourceUri = `ui://${safeId}/widget.${hash}.html`;
890
+ const options = {
891
+ description: "Renders the selected result from a Sidecar code-mode execution.",
892
+ csp: {
893
+ connectDomains: [],
894
+ resourceDomains: []
895
+ }
896
+ };
897
+ await mkdir(outputDir, { recursive: true });
898
+ await writeFile(outputFile, html);
899
+ return {
900
+ sourceFile: path4.relative(rootDir, entryFile),
901
+ variant: "shared",
902
+ resourceUri,
903
+ resourceMeta: widgetResourceMeta(options, target),
904
+ outputFile: path4.relative(outDir, outputFile),
905
+ options
906
+ };
907
+ }
994
908
  function widgetBuildOptionsFromConfig(rootDir, config) {
995
909
  const esbuildConfig = config?.esbuild;
996
910
  if (!esbuildConfig) {
@@ -1087,6 +1001,105 @@ function enforceWidgetBuildInvariants(options, required) {
1087
1001
  write: false
1088
1002
  };
1089
1003
  }
1004
+ function renderCodeModeWidgetEntry(rootDir, entryFile, entries, appStyle) {
1005
+ const entryDir = path4.dirname(entryFile);
1006
+ const imports = entries.map((entry, index) => {
1007
+ const sourceFile = path4.join(rootDir, entry.widget.sourceFile);
1008
+ return `import Widget${index} from ${JSON.stringify(toImportSpecifier(entryDir, sourceFile))};`;
1009
+ }).join("\n");
1010
+ const registry = entries.map((entry, index) => `${JSON.stringify(entry.id)}: Widget${index}`).join(",\n ");
1011
+ return `import React, { useMemo } from "react";
1012
+ import { createRoot } from "react-dom/client";
1013
+ import {
1014
+ SidecarWidgetProvider,
1015
+ SidecarWidgetRoot,
1016
+ browserBridge,
1017
+ useToolResult
1018
+ } from "@sidecar-ai/react";
1019
+ import { Heading, Stack, Surface, Text } from "@sidecar-ai/native/components";
1020
+ import "@sidecar-ai/native/styles.css";
1021
+ ${appStyle ? `import ${JSON.stringify(toImportSpecifier(entryDir, appStyle))};` : ""}
1022
+ ${imports}
1023
+
1024
+ const registry = {
1025
+ ${registry}
1026
+ };
1027
+
1028
+ function nestedResult(result, payload) {
1029
+ return {
1030
+ ...result,
1031
+ structuredContent: payload?.result ?? payload?.value,
1032
+ structured: payload?.result ?? payload?.value,
1033
+ content: payload?.content ?? result.content ?? [],
1034
+ meta: payload?.meta ?? result.meta ?? {},
1035
+ _meta: payload?.meta ?? result._meta ?? {},
1036
+ };
1037
+ }
1038
+
1039
+ function createNestedBridge(result, payload) {
1040
+ return {
1041
+ ...browserBridge,
1042
+ getToolResult() {
1043
+ return nestedResult(result, payload);
1044
+ },
1045
+ subscribeToolResult(listener) {
1046
+ return browserBridge.subscribeToolResult((next) => {
1047
+ const nextPayload = next?.structuredContent?.codeMode;
1048
+ listener(nestedResult(next, nextPayload));
1049
+ });
1050
+ },
1051
+ };
1052
+ }
1053
+
1054
+ function GenericCodeModeResult({ payload }) {
1055
+ return React.createElement("main", { className: "sidecar-code-mode" },
1056
+ React.createElement(Stack, { gap: "md" },
1057
+ React.createElement(Heading, { level: 1 }, "Code result"),
1058
+ React.createElement(Surface, { variant: "card" },
1059
+ React.createElement("pre", {
1060
+ style: {
1061
+ margin: 0,
1062
+ overflowWrap: "anywhere",
1063
+ whiteSpace: "pre-wrap"
1064
+ }
1065
+ }, JSON.stringify(payload?.value ?? payload ?? {}, null, 2))
1066
+ )
1067
+ )
1068
+ );
1069
+ }
1070
+
1071
+ function CodeModeWidget() {
1072
+ const result = useToolResult();
1073
+ const payload = result.structuredContent?.codeMode;
1074
+ const renderer = payload?.renderer;
1075
+ const Component = renderer ? registry[renderer] : undefined;
1076
+ const bridge = useMemo(() => createNestedBridge(result, payload), [result, payload]);
1077
+
1078
+ if (Component) {
1079
+ return React.createElement(
1080
+ SidecarWidgetProvider,
1081
+ { bridge },
1082
+ React.createElement(Component)
1083
+ );
1084
+ }
1085
+
1086
+ if (payload?.ok === false) {
1087
+ return React.createElement("main", { className: "sidecar-code-mode" },
1088
+ React.createElement(Stack, { gap: "sm" },
1089
+ React.createElement(Heading, { level: 1 }, "Code failed"),
1090
+ React.createElement(Text, { tone: "muted" }, "The generated code did not complete successfully.")
1091
+ )
1092
+ );
1093
+ }
1094
+
1095
+ return React.createElement(GenericCodeModeResult, { payload });
1096
+ }
1097
+
1098
+ createRoot(document.getElementById("root")).render(
1099
+ React.createElement(SidecarWidgetRoot, null, React.createElement(CodeModeWidget))
1100
+ );
1101
+ `;
1102
+ }
1090
1103
  function sidecarWidgetAliases(rootDir) {
1091
1104
  return {
1092
1105
  ...devSidecarBundleAliases(rootDir) ?? {},
@@ -1135,6 +1148,7 @@ function devSidecarBundleAliases(rootDir) {
1135
1148
  return void 0;
1136
1149
  }
1137
1150
  return {
1151
+ "sidecar-ai/remote": path4.join(repoRoot, "packages", "sidecar-ai", "src", "remote.ts"),
1138
1152
  "sidecar-ai": path4.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
1139
1153
  "@sidecar-ai/client": path4.join(repoRoot, "packages", "client", "src", "index.ts"),
1140
1154
  "@sidecar-ai/core": path4.join(repoRoot, "packages", "core", "src", "index.ts"),
@@ -1896,6 +1910,7 @@ function readStringArrayProperty2(definition, propertyName) {
1896
1910
 
1897
1911
  // packages/compiler/src/build.ts
1898
1912
  import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
1913
+ import { existsSync as existsSync7 } from "fs";
1899
1914
  import path19 from "path";
1900
1915
 
1901
1916
  // packages/compiler/src/config.ts
@@ -1937,6 +1952,10 @@ function analyzeProjectConfig(rootDir) {
1937
1952
  pagination: {
1938
1953
  pageSize: readNumberNested(definition, "pagination", "pageSize") ?? 50,
1939
1954
  hasOverride: hasProperty(readObjectProperty3(definition, "pagination"), "override")
1955
+ },
1956
+ codeMode: readCodeModeConfig(definition),
1957
+ remoteExecution: {
1958
+ enabled: readBooleanProperty3(definition, "remoteExecution") ?? false
1940
1959
  }
1941
1960
  };
1942
1961
  }
@@ -1994,9 +2013,75 @@ function defaultCompilerConfig() {
1994
2013
  pagination: {
1995
2014
  pageSize: 50,
1996
2015
  hasOverride: false
2016
+ },
2017
+ codeMode: {
2018
+ enabled: false,
2019
+ unsafe: false,
2020
+ render: {
2021
+ enabled: true,
2022
+ strategy: "last-renderable"
2023
+ }
2024
+ },
2025
+ remoteExecution: {
2026
+ enabled: false
1997
2027
  }
1998
2028
  };
1999
2029
  }
2030
+ function readCodeModeConfig(definition) {
2031
+ const defaults = defaultCompilerConfig().codeMode;
2032
+ const property = definition.getProperty("codeMode");
2033
+ if (!property || !Node5.isPropertyAssignment(property)) {
2034
+ return defaults;
2035
+ }
2036
+ const initializer = unwrapExpression(property.getInitializer());
2037
+ if (!initializer) {
2038
+ return defaults;
2039
+ }
2040
+ if (initializer.getKind() === SyntaxKind3.TrueKeyword) {
2041
+ return { ...defaults, enabled: true };
2042
+ }
2043
+ if (initializer.getKind() === SyntaxKind3.FalseKeyword) {
2044
+ return { ...defaults, enabled: false };
2045
+ }
2046
+ if (!Node5.isObjectLiteralExpression(initializer)) {
2047
+ return defaults;
2048
+ }
2049
+ return {
2050
+ enabled: true,
2051
+ unsafe: readBooleanProperty3(initializer, "unsafe") ?? false,
2052
+ render: readCodeModeRenderConfig(initializer) ?? defaults.render
2053
+ };
2054
+ }
2055
+ function readCodeModeRenderConfig(definition) {
2056
+ const property = definition.getProperty("render");
2057
+ if (!property || !Node5.isPropertyAssignment(property)) {
2058
+ return void 0;
2059
+ }
2060
+ const initializer = unwrapExpression(property.getInitializer());
2061
+ if (!initializer) {
2062
+ return void 0;
2063
+ }
2064
+ if (initializer.getKind() === SyntaxKind3.TrueKeyword) {
2065
+ return { enabled: true, strategy: "last-renderable" };
2066
+ }
2067
+ if (initializer.getKind() === SyntaxKind3.FalseKeyword) {
2068
+ return { enabled: false, strategy: "last-renderable" };
2069
+ }
2070
+ if (!Node5.isObjectLiteralExpression(initializer)) {
2071
+ return void 0;
2072
+ }
2073
+ return {
2074
+ enabled: readBooleanProperty3(initializer, "enabled") ?? true,
2075
+ strategy: readRenderStrategy(initializer) ?? "last-renderable"
2076
+ };
2077
+ }
2078
+ function readRenderStrategy(definition) {
2079
+ const value = readStringProperty3(definition, "strategy");
2080
+ if (value === "last-renderable" || value === "first-renderable" || value === "explicit") {
2081
+ return value;
2082
+ }
2083
+ return void 0;
2084
+ }
2000
2085
  function readTargetNested(definition, section, propertyName) {
2001
2086
  const value = readStringNested(definition, section, propertyName);
2002
2087
  return value === "mcp" || value === "chatgpt" || value === "claude" ? value : void 0;
@@ -2017,7 +2102,10 @@ function readBooleanNested(definition, section, propertyName) {
2017
2102
  if (!object) {
2018
2103
  return void 0;
2019
2104
  }
2020
- const property = object.getProperty(propertyName);
2105
+ return readBooleanProperty3(object, propertyName);
2106
+ }
2107
+ function readBooleanProperty3(definition, propertyName) {
2108
+ const property = definition.getProperty(propertyName);
2021
2109
  if (!property || !Node5.isPropertyAssignment(property)) {
2022
2110
  return void 0;
2023
2111
  }
@@ -3046,12 +3134,12 @@ function readArgInput(node) {
3046
3134
  if (Node7.isObjectLiteralExpression(initializer)) {
3047
3135
  return {
3048
3136
  description: getOptionalStringProperty2(initializer, "description"),
3049
- required: readBooleanProperty3(initializer, "required")
3137
+ required: readBooleanProperty4(initializer, "required")
3050
3138
  };
3051
3139
  }
3052
3140
  return void 0;
3053
3141
  }
3054
- function readBooleanProperty3(definition, propertyName) {
3142
+ function readBooleanProperty4(definition, propertyName) {
3055
3143
  const property = definition.getProperty(propertyName);
3056
3144
  if (!property || !Node7.isPropertyAssignment(property)) {
3057
3145
  return void 0;
@@ -3170,7 +3258,7 @@ function analyzeResourceFile(sourceFile, rootDir) {
3170
3258
  mimeType: descriptor.mimeType,
3171
3259
  size: descriptor.size,
3172
3260
  annotations: descriptor.annotations,
3173
- subscribe: readBooleanProperty4(definition, "subscribe"),
3261
+ subscribe: readBooleanProperty5(definition, "subscribe"),
3174
3262
  descriptor
3175
3263
  };
3176
3264
  }
@@ -3231,7 +3319,7 @@ function getOptionalNumberProperty(definition, propertyName) {
3231
3319
  const initializer = unwrapExpression(property.getInitializer());
3232
3320
  return initializer && Node8.isNumericLiteral(initializer) ? Number(initializer.getLiteralText()) : void 0;
3233
3321
  }
3234
- function readBooleanProperty4(definition, propertyName) {
3322
+ function readBooleanProperty5(definition, propertyName) {
3235
3323
  const property = definition.getProperty(propertyName);
3236
3324
  if (!property || !Node8.isPropertyAssignment(property)) {
3237
3325
  return void 0;
@@ -3379,7 +3467,7 @@ function renderServerEntry(rootDir, entryFile, manifest, identity) {
3379
3467
  `import { pathToFileURL } from "node:url";`,
3380
3468
  `import { createSidecarHttpHandler } from "@sidecar-ai/server";`,
3381
3469
  `import { isSidecarAuth } from "@sidecar-ai/auth";`,
3382
- `import { isSidecarPrompt, isSidecarResource, isSidecarTool } from "@sidecar-ai/core";`,
3470
+ `import { isSidecarPrompt, isSidecarRemote, isSidecarResource, isSidecarTool } from "@sidecar-ai/core";`,
3383
3471
  `import { isSidecarProxy } from "@sidecar-ai/server/proxy";`,
3384
3472
  ...tools.map(
3385
3473
  (entry, index) => `import tool${index} from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, entry.sourceFile)))};`
@@ -3392,6 +3480,7 @@ function renderServerEntry(rootDir, entryFile, manifest, identity) {
3392
3480
  ),
3393
3481
  existsSync6(path18.join(rootDir, "auth.ts")) ? `import authExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "auth.ts")))};` : `const authExport = undefined;`,
3394
3482
  existsSync6(path18.join(rootDir, "proxy.ts")) ? `import proxyExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "proxy.ts")))};` : `const proxyExport = undefined;`,
3483
+ existsSync6(path18.join(rootDir, "remote.ts")) ? `import remoteExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "remote.ts")))};` : `const remoteExport = undefined;`,
3395
3484
  existsSync6(path18.join(rootDir, "sidecar.config.ts")) ? `import configExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "sidecar.config.ts")))};` : `const configExport = undefined;`
3396
3485
  ].join("\n");
3397
3486
  return `${imports}
@@ -3404,6 +3493,9 @@ const auth = loadedAuth && process.env.SIDECAR_MCP_URL
3404
3493
  ? loadedAuth.withResource(process.env.SIDECAR_MCP_URL)
3405
3494
  : loadedAuth;
3406
3495
  const proxy = proxyExport === undefined ? undefined : assertProxy(proxyExport);
3496
+ const remoteExecution = manifest.codeMode?.remoteExecution
3497
+ ? assertRemote(remoteExport)
3498
+ : undefined;
3407
3499
  const runtimeConfig = configExport && typeof configExport === "object" ? configExport : undefined;
3408
3500
 
3409
3501
  if (auth && !process.env.SIDECAR_MCP_URL) {
@@ -3417,6 +3509,21 @@ export const handler = createSidecarHttpHandler({
3417
3509
  publicUrl: process.env.SIDECAR_PUBLIC_URL ?? process.env.SIDECAR_MCP_URL,
3418
3510
  auth,
3419
3511
  proxy,
3512
+ codeMode: manifest.codeMode
3513
+ ? {
3514
+ enabled: true,
3515
+ unsafe: manifest.codeMode.unsafe,
3516
+ render: manifest.codeMode.render,
3517
+ widgetMeta: manifest.codeMode.widget?.resourceUri
3518
+ ? {
3519
+ ui: { resourceUri: manifest.codeMode.widget.resourceUri },
3520
+ "ui/resourceUri": manifest.codeMode.widget.resourceUri,
3521
+ ...(manifest.target === "chatgpt" ? { "openai/outputTemplate": manifest.codeMode.widget.resourceUri } : {}),
3522
+ }
3523
+ : undefined,
3524
+ }
3525
+ : undefined,
3526
+ remoteExecution,
3420
3527
  tools: [
3421
3528
  ${tools.map((entry, index) => ` loadTool(${JSON.stringify(entry.sourceFile)}, tool${index}, manifest.tools[${index}].descriptor),`).join("\n")}
3422
3529
  ],
@@ -3448,12 +3555,23 @@ if (isDirectRun()) {
3448
3555
  server.listen(port, host, () => {
3449
3556
  const localHost = host === "0.0.0.0" ? "127.0.0.1" : host;
3450
3557
  console.log(\`MCP running on Streamable HTTP at http://\${localHost}:\${port}\${process.env.SIDECAR_MCP_PATH ?? "/mcp"}\`);
3558
+ printRuntimeUrls(localHost, port);
3451
3559
  });
3452
3560
 
3453
3561
  process.on("SIGTERM", () => shutdown());
3454
3562
  process.on("SIGINT", () => shutdown());
3455
3563
  }
3456
3564
 
3565
+ function printRuntimeUrls(localHost, port) {
3566
+ const mcpPath = process.env.SIDECAR_MCP_PATH ?? "/mcp";
3567
+ console.log("Sidecar URLs:");
3568
+ console.log(\` Local MCP: http://\${localHost}:\${port}\${mcpPath}\`);
3569
+ console.log(\` Public MCP: \${process.env.SIDECAR_MCP_URL ?? "set SIDECAR_MCP_URL=https://your-host.example.com" + mcpPath}\`);
3570
+ if (process.env.SIDECAR_PUBLIC_URL) {
3571
+ console.log(\` Public base: \${process.env.SIDECAR_PUBLIC_URL}\`);
3572
+ }
3573
+ }
3574
+
3457
3575
  function loadTool(sourceFile, value, descriptor) {
3458
3576
  const tool = unwrapRuntimeDefault(value);
3459
3577
  if (!isSidecarTool(tool)) {
@@ -3498,6 +3616,14 @@ function assertProxy(value) {
3498
3616
  return proxyValue;
3499
3617
  }
3500
3618
 
3619
+ function assertRemote(value) {
3620
+ const remoteValue = unwrapRuntimeDefault(value);
3621
+ if (!isSidecarRemote(remoteValue)) {
3622
+ throw new Error("remote.ts must default-export remote({ execute }) from sidecar-ai/remote.");
3623
+ }
3624
+ return remoteValue;
3625
+ }
3626
+
3501
3627
  function unwrapRuntimeDefault(value) {
3502
3628
  if (
3503
3629
  value &&
@@ -3538,7 +3664,7 @@ function shutdown() {
3538
3664
  `;
3539
3665
  }
3540
3666
  function renderWidgetResources(manifest) {
3541
- return manifest.tools.filter((entry) => entry.widget?.outputFile).map((entry) => ` {
3667
+ const toolWidgets = manifest.tools.filter((entry) => entry.widget?.outputFile).map((entry) => ` {
3542
3668
  uri: ${JSON.stringify(entry.widget?.resourceUri)},
3543
3669
  name: ${JSON.stringify(entry.name)},
3544
3670
  description: ${JSON.stringify(entry.widget?.options?.description)},
@@ -3546,6 +3672,15 @@ function renderWidgetResources(manifest) {
3546
3672
  text: readWidget(${JSON.stringify(entry.widget?.outputFile)}),
3547
3673
  _meta: ${JSON.stringify(entry.widget?.resourceMeta ?? void 0)},
3548
3674
  },`).join("\n");
3675
+ const codeModeWidget = manifest.codeMode?.widget?.outputFile ? ` {
3676
+ uri: ${JSON.stringify(manifest.codeMode.widget.resourceUri)},
3677
+ name: "Execute Code",
3678
+ description: ${JSON.stringify(manifest.codeMode.widget.options?.description)},
3679
+ mimeType: "text/html;profile=mcp-app",
3680
+ text: readWidget(${JSON.stringify(manifest.codeMode.widget.outputFile)}),
3681
+ _meta: ${JSON.stringify(manifest.codeMode.widget.resourceMeta ?? void 0)},
3682
+ },` : "";
3683
+ return [codeModeWidget, toolWidgets].filter(Boolean).join("\n");
3549
3684
  }
3550
3685
  function renderServerPackage(identity) {
3551
3686
  return `${JSON.stringify({
@@ -3595,6 +3730,7 @@ function sidecarBundleAliases(rootDir) {
3595
3730
  return void 0;
3596
3731
  }
3597
3732
  return {
3733
+ "sidecar-ai/remote": path18.join(repoRoot, "packages", "sidecar-ai", "src", "remote.ts"),
3598
3734
  "sidecar-ai": path18.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
3599
3735
  "@sidecar-ai/auth": path18.join(repoRoot, "packages", "auth", "src", "index.ts"),
3600
3736
  "@sidecar-ai/core": path18.join(repoRoot, "packages", "core", "src", "index.ts"),
@@ -3638,6 +3774,12 @@ async function buildProject(options) {
3638
3774
  const target = options.target ?? config.build.target ?? "mcp";
3639
3775
  const host = options.host ?? config.build.host ?? "node";
3640
3776
  const plugins = options.plugins ?? config.build.plugins ?? false;
3777
+ if (config.codeMode.enabled && !config.codeMode.unsafe && !config.remoteExecution.enabled) {
3778
+ throw new Error("Code mode requires remoteExecution: true, or codeMode: { unsafe: true } for trusted local use.");
3779
+ }
3780
+ if (config.codeMode.enabled && config.remoteExecution.enabled && !existsSync7(path19.join(rootDir, "remote.ts"))) {
3781
+ throw new Error("remoteExecution: true requires a reserved remote.ts file at the project root.");
3782
+ }
3641
3783
  const tools = await analyzeProjectTools(rootDir, { target });
3642
3784
  const resources = await analyzeProjectResources(rootDir);
3643
3785
  const resourceTemplates = [];
@@ -3656,6 +3798,7 @@ async function buildProject(options) {
3656
3798
  const outDir = resolveInsideRoot(rootDir, options.outDir ?? config.build.outDir ?? defaultBuildOutDir(host, target));
3657
3799
  const runtimeOutDir = resolveRuntimeOutputDir(outDir, host);
3658
3800
  await buildWidgets(rootDir, runtimeOutDir, tools, config.build.widgets);
3801
+ const codeModeWidget = config.codeMode.enabled && config.codeMode.render.enabled ? await buildCodeModeWidget(rootDir, runtimeOutDir, tools, target, config.build.widgets) : void 0;
3659
3802
  const manifest = {
3660
3803
  version: 1,
3661
3804
  target,
@@ -3664,6 +3807,13 @@ async function buildProject(options) {
3664
3807
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3665
3808
  config,
3666
3809
  tools,
3810
+ codeMode: config.codeMode.enabled ? {
3811
+ enabled: true,
3812
+ unsafe: config.codeMode.unsafe,
3813
+ remoteExecution: config.remoteExecution.enabled,
3814
+ render: config.codeMode.render,
3815
+ widget: codeModeWidget
3816
+ } : void 0,
3667
3817
  resources,
3668
3818
  resourceTemplates,
3669
3819
  prompts,
@@ -3768,1359 +3918,1723 @@ This output uses Vercel's Build Output API. The MCP function is emitted at \`${V
3768
3918
 
3769
3919
  // packages/server/src/index.ts
3770
3920
  import { createServer } from "http";
3771
- import { randomUUID as randomUUID2 } from "crypto";
3921
+ import {
3922
+ createCipheriv,
3923
+ createDecipheriv,
3924
+ createHash as createHash2,
3925
+ randomBytes,
3926
+ randomUUID as randomUUID2
3927
+ } from "crypto";
3772
3928
 
3773
3929
  // packages/server/src/proxy.ts
3774
3930
  import { randomUUID } from "crypto";
3775
- var proxyBrand = /* @__PURE__ */ Symbol.for("sidecar.proxy");
3776
- function isSidecarProxy(value) {
3777
- return Boolean(
3778
- value && typeof value === "object" && (value[proxyBrand] === true || value.kind === "sidecar.proxy")
3779
- );
3780
- }
3781
- async function runProxy(proxyConfig, request) {
3782
- for (const middleware of proxyConfig?.before ?? []) {
3783
- const result = await middleware(request);
3784
- if (result) {
3785
- return result;
3786
- }
3787
- }
3788
- return void 0;
3789
- }
3790
3931
 
3791
3932
  // packages/server/src/sse.ts
3792
3933
  import { JSONRPC_VERSION } from "@modelcontextprotocol/sdk/types.js";
3793
- var sseEventCounter = 0;
3794
- function createSseHub() {
3795
- const streams = /* @__PURE__ */ new Set();
3796
- return {
3797
- open(response) {
3798
- const stream = createSseStream(response);
3799
- streams.add(stream);
3800
- response.on("close", () => {
3801
- streams.delete(stream);
3802
- });
3803
- return stream;
3804
- },
3805
- send(method, params) {
3806
- const stream = streams.values().next().value;
3807
- stream?.send(method, params);
3808
- }
3934
+
3935
+ // packages/server/src/index.ts
3936
+ import { JSONRPC_VERSION as JSONRPC_VERSION2 } from "@modelcontextprotocol/sdk/types.js";
3937
+ var CODE_MODE_CALLBACK_TOKEN_TTL_MS = 5 * 60 * 1e3;
3938
+
3939
+ // packages/cli/src/dev-harness.ts
3940
+ import { randomUUID as randomUUID3 } from "crypto";
3941
+ import { readFileSync } from "fs";
3942
+ import { createServer as createServer2 } from "http";
3943
+ import { createRequire as createRequire2 } from "module";
3944
+ import path20 from "path";
3945
+ import { fileURLToPath } from "url";
3946
+ var require2 = createRequire2(import.meta.url);
3947
+ var OPENAI_CHAT_COMPLETIONS_URL = "https://api.openai.com/v1/chat/completions";
3948
+ var MCP_PROTOCOL_VERSION = "2025-11-25";
3949
+ var MCP_APPS_PROTOCOL_VERSION = "2026-01-26";
3950
+ var MAX_CHAT_TOOL_ITERATIONS = 4;
3951
+ var streamdownClientScriptPromise;
3952
+ async function startDevHarness(options) {
3953
+ const state = {
3954
+ host: options.host,
3955
+ theme: options.theme,
3956
+ device: options.device,
3957
+ target: options.target,
3958
+ model: options.model
3809
3959
  };
3810
- }
3811
- function createSseStream(response, options = {}) {
3812
- let closed = false;
3813
- response.writeHead(200, {
3814
- "content-type": "text/event-stream",
3815
- "cache-control": "no-cache, no-transform",
3816
- "connection": "keep-alive",
3817
- "x-accel-buffering": "no"
3818
- });
3819
- response.flushHeaders?.();
3820
- const keepAlive = setInterval(() => {
3821
- writeRaw(": keepalive\n\n");
3822
- }, 3e4);
3823
- keepAlive.unref?.();
3824
- response.on("close", () => {
3825
- closed = true;
3826
- clearInterval(keepAlive);
3960
+ const stateClients = /* @__PURE__ */ new Set();
3961
+ const server = createServer2(async (request, response) => {
3962
+ try {
3963
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
3964
+ if (request.method === "GET" && url.pathname === "/") {
3965
+ sendHtml(response, renderDevHarnessHtml(state, { initialBearerToken: options.initialBearerToken }));
3966
+ return;
3967
+ }
3968
+ if (request.method === "GET" && url.pathname === "/__sidecar/dev/streamdown-client.js") {
3969
+ response.writeHead(200, {
3970
+ "cache-control": "no-cache",
3971
+ "content-type": "text/javascript; charset=utf-8"
3972
+ });
3973
+ response.end(await streamdownClientScript());
3974
+ return;
3975
+ }
3976
+ if (request.method === "GET" && url.pathname === "/__sidecar/dev/state") {
3977
+ sendJson(response, devStatePayload(state, options.mcpUrl));
3978
+ return;
3979
+ }
3980
+ if (request.method === "POST" && url.pathname === "/__sidecar/dev/state") {
3981
+ const body = await readJson(request);
3982
+ updateDevState(state, body);
3983
+ sendJson(response, devStatePayload(state, options.mcpUrl));
3984
+ broadcastState(stateClients, state, options.mcpUrl);
3985
+ return;
3986
+ }
3987
+ if (request.method === "GET" && url.pathname === "/__sidecar/dev/events") {
3988
+ openStateEvents(response, stateClients, state, options.mcpUrl);
3989
+ return;
3990
+ }
3991
+ if (request.method === "GET" && url.pathname === "/__sidecar/dev/resource") {
3992
+ const uri = url.searchParams.get("uri");
3993
+ const authToken = readBearerFromCookie(request);
3994
+ if (!uri) {
3995
+ sendJson(response, { error: "missing_resource_uri" }, 400);
3996
+ return;
3997
+ }
3998
+ const resource = await readMcpResource(options.mcpUrl, uri, authToken);
3999
+ response.writeHead(200, { "content-type": resource.mimeType ?? "text/html; charset=utf-8" });
4000
+ response.end(resource.text ?? "");
4001
+ return;
4002
+ }
4003
+ if (request.method === "POST" && url.pathname === "/__sidecar/dev/rpc") {
4004
+ const body = await readJson(request);
4005
+ const method = typeof body.method === "string" ? body.method : void 0;
4006
+ if (!method) {
4007
+ sendJson(response, { error: "missing_method" }, 400);
4008
+ return;
4009
+ }
4010
+ const result = await callMcp(options.mcpUrl, method, body.params, readAuthToken(body.authToken));
4011
+ sendJson(response, { result });
4012
+ return;
4013
+ }
4014
+ if (request.method === "POST" && url.pathname === "/__sidecar/dev/chat") {
4015
+ await handleChatRequest(request, response, options);
4016
+ return;
4017
+ }
4018
+ sendJson(response, { error: "not_found" }, 404);
4019
+ } catch (error) {
4020
+ sendJson(response, normalizeHttpError(error), error instanceof HttpError ? error.status : 500);
4021
+ }
3827
4022
  });
3828
- writeRaw(`id: ${nextSseEventId()}
3829
- data:
3830
-
3831
- `);
3832
- function writeRaw(frame) {
3833
- if (!closed && !response.destroyed) {
3834
- response.write(frame);
3835
- }
3836
- }
3837
- function sendJson(value) {
3838
- writeRaw([
3839
- `id: ${nextSseEventId()}`,
3840
- "event: message",
3841
- `data: ${JSON.stringify(value)}`,
3842
- "",
3843
- ""
3844
- ].join("\n"));
3845
- }
4023
+ const port = await listenOnLocalhost(server, options.port);
3846
4024
  return {
3847
- supportsRequestProgress: options.supportsRequestProgress,
3848
- send(method, params) {
3849
- sendJson(omitUndefined({
3850
- jsonrpc: JSONRPC_VERSION,
3851
- method,
3852
- params
3853
- }));
3854
- },
3855
- sendJson,
3856
- end() {
3857
- closed = true;
3858
- clearInterval(keepAlive);
3859
- response.end();
3860
- }
4025
+ port,
4026
+ url: `http://127.0.0.1:${port}`,
4027
+ close: () => closeServer(server)
3861
4028
  };
3862
4029
  }
3863
- function nextSseEventId() {
3864
- sseEventCounter += 1;
3865
- return `sidecar-${Date.now()}-${sseEventCounter}`;
4030
+ function mcpToolsToOpenAiTools(tools) {
4031
+ const used = /* @__PURE__ */ new Set();
4032
+ const byOpenAiName = /* @__PURE__ */ new Map();
4033
+ const openAiTools = tools.map((tool) => {
4034
+ const name = uniqueOpenAiToolName(tool.name, used);
4035
+ byOpenAiName.set(name, tool);
4036
+ return {
4037
+ type: "function",
4038
+ function: {
4039
+ name,
4040
+ description: tool.description ?? tool.title ?? tool.name,
4041
+ parameters: normalizeToolParameters(tool.inputSchema)
4042
+ }
4043
+ };
4044
+ });
4045
+ return { byOpenAiName, tools: openAiTools };
3866
4046
  }
3867
- function omitUndefined(value) {
3868
- return Object.fromEntries(
3869
- Object.entries(value).filter(([, entry]) => entry !== void 0)
3870
- );
4047
+ function toolResourceUri(tool) {
4048
+ const meta = tool._meta;
4049
+ const nested = meta?.ui && typeof meta.ui === "object" ? meta.ui.resourceUri : void 0;
4050
+ const uri = nested ?? meta?.["ui/resourceUri"] ?? meta?.["openai/outputTemplate"];
4051
+ return typeof uri === "string" && uri ? uri : void 0;
3871
4052
  }
3872
-
3873
- // packages/server/src/index.ts
3874
- import { JSONRPC_VERSION as JSONRPC_VERSION2 } from "@modelcontextprotocol/sdk/types.js";
3875
- var SIDECAR_MCP_PROTOCOL_VERSION = "2025-11-25";
3876
- var SidecarMcpServer = class {
3877
- constructor(options) {
3878
- this.options = options;
3879
- for (const loaded of options.tools) {
3880
- const descriptorProvided = Boolean(loaded.descriptor);
3881
- const descriptor = loaded.descriptor ?? createToolDescriptor(loaded.tool);
3882
- if (this.tools.has(descriptor.name)) {
3883
- throw new Error(`Duplicate Sidecar tool id "${descriptor.name}".`);
4053
+ function renderDevHarnessHtml(state, options = {}) {
4054
+ return `<!doctype html>
4055
+ <html lang="en" data-sidecar-host="${state.host}" data-sidecar-theme="${state.theme}" data-sidecar-device="${state.device}">
4056
+ <head>
4057
+ <meta charset="utf-8" />
4058
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4059
+ <title>Sidecar dev</title>
4060
+ <style>
4061
+ :root {
4062
+ color-scheme: light dark;
4063
+ --radius: 14px;
4064
+ --bg: oklch(0.971 0.025 97.1);
4065
+ --text: oklch(0.145 0 0);
4066
+ --panel: oklch(1 0 0);
4067
+ --panel-soft: oklch(0.903 0.033 93.5);
4068
+ --muted: oklch(0.537 0.022 97.1);
4069
+ --border: oklch(0.911 0.035 96.3);
4070
+ --sidebar: oklch(0.959 0.035 96.3);
4071
+ --accent: oklch(0.651 0.055 65.6);
4072
+ --accent-text: oklch(0.984 0.014 78.3);
4073
+ --control-bg: oklch(0.145 0 0);
4074
+ --control-text: oklch(0.985 0 0);
4075
+ --send-bg: oklch(0.145 0 0);
4076
+ --send-text: oklch(0.985 0 0);
4077
+ --ring: oklch(0.708 0 0 / 28%);
4078
+ --widget-bg: oklch(1 0 0);
4079
+ --sidebar-width: 80px;
4080
+ --chat-width: 420px;
4081
+ --mobile-width: min(430px, calc(100vw - 28px));
4082
+ --shadow: 0 24px 70px rgb(15 23 42 / 13%);
3884
4083
  }
3885
- this.tools.set(descriptor.name, { ...loaded, descriptor, descriptorProvided });
3886
- }
3887
- for (const resource of options.resources ?? []) {
3888
- if (this.resources.has(resource.uri)) {
3889
- throw new Error(`Duplicate Sidecar resource uri "${resource.uri}".`);
4084
+ :root[data-sidecar-theme="dark"] {
4085
+ --bg: oklch(0.218 0 0);
4086
+ --text: oklch(0.985 0 0);
4087
+ --panel: oklch(0.205 0 0);
4088
+ --panel-soft: oklch(0.269 0 0);
4089
+ --muted: oklch(0.708 0 0);
4090
+ --border: oklch(1 0 0 / 10%);
4091
+ --sidebar: oklch(0.264 0 0);
4092
+ --accent: oklch(0.755 0.049 61.8);
4093
+ --accent-text: oklch(0.218 0 0);
4094
+ --control-bg: oklch(0.985 0 0);
4095
+ --control-text: oklch(0.218 0 0);
4096
+ --send-bg: oklch(0.985 0 0);
4097
+ --send-text: oklch(0.218 0 0);
4098
+ --ring: oklch(0.556 0 0 / 34%);
4099
+ --widget-bg: oklch(0.205 0 0);
4100
+ --shadow: 0 24px 70px rgb(0 0 0 / 34%);
3890
4101
  }
3891
- const descriptor = resource.descriptor ?? {
3892
- uri: resource.uri,
3893
- name: resource.name ?? resource.uri,
3894
- description: resource.description,
3895
- mimeType: resource.mimeType,
3896
- _meta: resource._meta
3897
- };
3898
- this.resources.set(resource.uri, { ...resource, descriptor });
3899
- }
3900
- for (const loaded of options.prompts ?? []) {
3901
- const descriptor = loaded.descriptor ?? {
3902
- name: loaded.prompt.name ?? loaded.prompt.title,
3903
- title: loaded.prompt.title,
3904
- description: loaded.prompt.description
3905
- };
3906
- if (this.prompts.has(descriptor.name)) {
3907
- throw new Error(`Duplicate Sidecar prompt name "${descriptor.name}".`);
4102
+ :root[data-sidecar-host="claude"] {
4103
+ --accent: #c96442;
4104
+ --accent-text: #fffaf4;
4105
+ --ring: rgb(201 100 66 / 24%);
3908
4106
  }
3909
- this.prompts.set(descriptor.name, { ...loaded, descriptor });
3910
- }
3911
- this.resourceTemplates = options.resourceTemplates ?? [];
3912
- }
3913
- options;
3914
- tools = /* @__PURE__ */ new Map();
3915
- resources = /* @__PURE__ */ new Map();
3916
- prompts = /* @__PURE__ */ new Map();
3917
- resourceTemplates;
3918
- activeRequests = /* @__PURE__ */ new Map();
3919
- subscribedResourceUris = /* @__PURE__ */ new Set();
3920
- /** Returns all tool descriptors exposed through `tools/list`. */
3921
- descriptors() {
3922
- return [...this.tools.values()].map((loaded) => loaded.descriptor);
3923
- }
3924
- /** Returns all resource descriptors exposed through `resources/list`. */
3925
- resourceDescriptors() {
3926
- return [...this.resources.values()].map((loaded) => loaded.descriptor);
3927
- }
3928
- /** Returns all prompt descriptors exposed through `prompts/list`. */
3929
- promptDescriptors() {
3930
- return [...this.prompts.values()].map((loaded) => loaded.descriptor);
3931
- }
3932
- /** Handles a JSON-RPC request or notification. */
3933
- async handle(request, context = {}) {
3934
- if (request.id === void 0) {
3935
- await this.authorizeEndpoint(context);
3936
- await this.handleNotification(request);
3937
- return void 0;
3938
- }
3939
- try {
3940
- const authSession = await this.authorizeEndpoint(context);
3941
- const authorizedContext = { ...context, authSession };
3942
- const result = await this.dispatch(request, authorizedContext);
3943
- return {
3944
- jsonrpc: JSONRPC_VERSION2,
3945
- id: request.id,
3946
- result
3947
- };
3948
- } catch (error) {
3949
- if (error instanceof RequestCancelledError) {
3950
- return void 0;
4107
+ :root[data-sidecar-host="claude"][data-sidecar-theme="dark"] {
4108
+ --accent: #d97757;
4109
+ --accent-text: #ffffff;
4110
+ --ring: rgb(217 119 87 / 28%);
3951
4111
  }
3952
- return {
3953
- jsonrpc: JSONRPC_VERSION2,
3954
- id: request.id,
3955
- error: normalizeError(error)
3956
- };
3957
- }
3958
- }
3959
- /** Routes an MCP method to its implementation. */
3960
- async dispatch(request, context) {
3961
- switch (request.method) {
3962
- case "initialize":
3963
- return {
3964
- protocolVersion: SIDECAR_MCP_PROTOCOL_VERSION,
3965
- capabilities: this.capabilities(),
3966
- serverInfo: {
3967
- name: this.options.name ?? "sidecar",
3968
- version: this.options.version ?? "0.0.0-dev"
3969
- }
3970
- };
3971
- case "ping":
3972
- return {};
3973
- case "tools/list":
3974
- return this.listTools(request, context);
3975
- case "tools/call":
3976
- return this.callTool(request, context);
3977
- case "resources/list":
3978
- return this.listResources(request, context);
3979
- case "resources/read":
3980
- return this.readResource(request, context);
3981
- case "resources/templates/list":
3982
- return this.listResourceTemplates(request, context);
3983
- case "resources/subscribe":
3984
- return this.subscribeResource(request);
3985
- case "resources/unsubscribe":
3986
- return this.unsubscribeResource(request);
3987
- case "prompts/list":
3988
- return this.listPrompts(request, context);
3989
- case "prompts/get":
3990
- return this.getPrompt(request, context);
3991
- default:
3992
- throw new JsonRpcError(-32601, `Unsupported method "${request.method}".`);
3993
- }
3994
- }
3995
- /** Builds the MCP server capability object from implemented runtime features. */
3996
- capabilities() {
3997
- const configured = this.options.capabilities ?? {};
3998
- return stripUndefined4({
3999
- tools: stripUndefined4({
4000
- listChanged: configured.tools?.listChanged || void 0
4001
- }),
4002
- resources: stripUndefined4({
4003
- subscribe: configured.resources?.subscribe || void 0,
4004
- listChanged: configured.resources?.listChanged || void 0
4005
- }),
4006
- prompts: this.prompts.size || configured.prompts?.listChanged ? stripUndefined4({
4007
- listChanged: configured.prompts?.listChanged || void 0
4008
- }) : void 0
4009
- });
4010
- }
4011
- /** Lists tools with MCP cursor pagination. */
4012
- async listTools(request, context) {
4013
- const page = await this.paginate("tools/list", this.descriptors(), request, context);
4014
- return stripUndefined4({
4015
- tools: [...page.items],
4016
- nextCursor: page.nextCursor
4017
- });
4018
- }
4019
- /** Lists resources with MCP cursor pagination. */
4020
- async listResources(request, context) {
4021
- const page = await this.paginate("resources/list", this.resourceDescriptors(), request, context);
4022
- return stripUndefined4({
4023
- resources: [...page.items],
4024
- nextCursor: page.nextCursor
4025
- });
4026
- }
4027
- /** Lists resource templates with MCP cursor pagination. */
4028
- async listResourceTemplates(request, context) {
4029
- const page = await this.paginate(
4030
- "resources/templates/list",
4031
- this.resourceTemplates.map((entry) => entry.descriptor),
4032
- request,
4033
- context
4034
- );
4035
- return stripUndefined4({
4036
- resourceTemplates: [...page.items],
4037
- nextCursor: page.nextCursor
4038
- });
4039
- }
4040
- /** Lists prompts with MCP cursor pagination. */
4041
- async listPrompts(request, context) {
4042
- const page = await this.paginate("prompts/list", this.promptDescriptors(), request, context);
4043
- return stripUndefined4({
4044
- prompts: [...page.items],
4045
- nextCursor: page.nextCursor
4046
- });
4047
- }
4048
- /** Applies configured or default cursor pagination to one supported list operation. */
4049
- async paginate(operation, items, request, context) {
4050
- const cursor = readCursorParam(request);
4051
- const pageSize = this.pageSize();
4052
- const override = selectPaginationOverride(this.options.pagination?.override, operation);
4053
- if (override) {
4054
- return override({
4055
- operation,
4056
- items,
4057
- cursor,
4058
- pageSize,
4059
- auth: context.authSession
4060
- });
4061
- }
4062
- return defaultPagination(items, cursor, pageSize);
4063
- }
4064
- /** Returns the server-chosen page size for all built-in list pagination. */
4065
- pageSize() {
4066
- const configured = this.options.pagination?.pageSize;
4067
- return configured && configured > 0 ? Math.floor(configured) : 50;
4068
- }
4069
- /** Executes a tool after request-level and tool-level auth checks. */
4070
- async callTool(request, context) {
4071
- const params = request.params;
4072
- const name = typeof params?.name === "string" ? params.name : void 0;
4073
- if (!name) {
4074
- throw new JsonRpcError(-32602, "tools/call requires params.name.");
4075
- }
4076
- const loaded = this.tools.get(name);
4077
- if (!loaded) {
4078
- throw new JsonRpcError(-32602, `Unknown tool "${name}".`);
4079
- }
4080
- const authSession = await this.authorizeTool(loaded, context);
4081
- const ctx = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
4082
- if (authSession !== void 0) {
4083
- ctx.auth = authSession;
4084
- }
4085
- ctx.notify = this.createNotifications(context, request);
4086
- const controller = new AbortController();
4087
- if (request.id !== null && request.id !== void 0) {
4088
- this.activeRequests.set(request.id, controller);
4089
- }
4090
- ctx.request = {
4091
- ...ctx.request,
4092
- signal: controller.signal
4093
- };
4094
- try {
4095
- const args = loaded.descriptorProvided ? validateAgainstSchema(
4096
- loaded.descriptor.inputSchema,
4097
- params?.arguments ?? {},
4098
- `Invalid parameters for tool "${name}".`
4099
- ) : params?.arguments ?? {};
4100
- const result = await withTimeout(
4101
- executeTool(loaded.tool, args, ctx),
4102
- this.options.toolTimeoutMs,
4103
- controller
4104
- );
4105
- if (loaded.descriptorProvided) {
4106
- validateAgainstSchema(
4107
- loaded.descriptor.outputSchema,
4108
- result.structuredContent,
4109
- `Invalid structuredContent returned by tool "${name}".`
4110
- );
4112
+ :root[data-sidecar-host="chatgpt"] {
4113
+ --accent: #10a37f;
4114
+ --accent-text: #ffffff;
4115
+ --ring: rgb(16 163 127 / 20%);
4111
4116
  }
4112
- return result;
4113
- } finally {
4114
- if (request.id !== null && request.id !== void 0) {
4115
- this.activeRequests.delete(request.id);
4117
+ * { box-sizing: border-box; }
4118
+ *::-webkit-scrollbar { height: 8px; width: 8px; }
4119
+ *::-webkit-scrollbar-track { background: transparent; }
4120
+ *::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--muted) 55%, transparent); border-radius: 999px; }
4121
+ body {
4122
+ background: var(--bg);
4123
+ color: var(--text);
4124
+ font: 14px/1.45 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4125
+ margin: 0;
4116
4126
  }
4117
- }
4118
- }
4119
- /** Authenticates the whole HTTP MCP request when auth.ts is configured. */
4120
- async authorizeEndpoint(context) {
4121
- if (context.authSession !== void 0) {
4122
- return context.authSession;
4123
- }
4124
- if (!this.options.auth) {
4125
- return void 0;
4126
- }
4127
- const request = context.request ?? new Request(this.options.auth.resource);
4128
- const requestAuth = await this.options.auth.authorizeRequest(request);
4129
- if (!requestAuth.ok) {
4130
- throw authJsonRpcError(requestAuth);
4131
- }
4132
- return requestAuth.auth;
4133
- }
4134
- /** Applies Sidecar auth policy for a tool call. */
4135
- async authorizeTool(loaded, context) {
4136
- const policy = loaded.tool.auth;
4137
- if (!this.options.auth) {
4138
- if (policy && policy.public !== true) {
4139
- throw new JsonRpcError(
4140
- -32001,
4141
- `Tool "${loaded.descriptor?.name ?? loaded.tool.name}" requires auth, but no auth.ts configuration is loaded.`
4142
- );
4127
+ .shell {
4128
+ display: flex;
4129
+ height: 100vh;
4130
+ min-height: 0;
4143
4131
  }
4144
- return void 0;
4145
- }
4146
- const authSession = context.authSession ?? await this.authorizeEndpoint(context);
4147
- const toolAuth = this.options.auth.authorizeTool(policy, authSession);
4148
- if (!toolAuth.ok) {
4149
- throw authJsonRpcError(toolAuth);
4150
- }
4151
- return toolAuth.auth;
4152
- }
4153
- /** Reads a generated widget or authored resource by URI. */
4154
- async readResource(request, context) {
4155
- const params = request.params;
4156
- const uri = typeof params?.uri === "string" ? params.uri : void 0;
4157
- if (!uri) {
4158
- throw new JsonRpcError(-32602, "resources/read requires params.uri.");
4159
- }
4160
- const resource = this.resources.get(uri);
4161
- if (!resource) {
4162
- throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
4163
- }
4164
- if (resource.resource) {
4165
- const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
4166
- if (context.authSession !== void 0) {
4167
- toolContext.auth = context.authSession;
4132
+ .content-shell {
4133
+ display: flex;
4134
+ flex-direction: column;
4135
+ flex: 1 1 auto;
4136
+ min-height: 0;
4137
+ min-width: 0;
4168
4138
  }
4169
- toolContext.notify = this.createNotifications(context, request);
4170
- return executeResource(resource.resource, toResourceContext(toolContext), {
4171
- uri,
4172
- mimeType: resource.descriptor.mimeType
4173
- });
4174
- }
4175
- return {
4176
- _meta: resource._meta,
4177
- contents: [
4178
- {
4179
- uri,
4180
- mimeType: resource.mimeType ?? "text/plain",
4181
- text: resource.text ?? "",
4182
- _meta: resource._meta
4183
- }
4184
- ]
4185
- };
4186
- }
4187
- /** Accepts a resource subscription when server-level support is enabled. */
4188
- subscribeResource(request) {
4189
- if (!this.options.capabilities?.resources?.subscribe) {
4190
- throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
4191
- }
4192
- const uri = readUriParam(request, "resources/subscribe");
4193
- if (!this.resources.has(uri)) {
4194
- throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
4195
- }
4196
- this.subscribedResourceUris.add(uri);
4197
- return {};
4198
- }
4199
- /** Accepts a resource unsubscription when server-level support is enabled. */
4200
- unsubscribeResource(request) {
4201
- if (!this.options.capabilities?.resources?.subscribe) {
4202
- throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
4203
- }
4204
- const uri = readUriParam(request, "resources/unsubscribe");
4205
- if (!this.resources.has(uri)) {
4206
- throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
4207
- }
4208
- this.subscribedResourceUris.delete(uri);
4209
- return {};
4210
- }
4211
- /** Renders one named prompt with validated arguments. */
4212
- async getPrompt(request, context) {
4213
- const params = request.params;
4214
- const name = typeof params?.name === "string" ? params.name : void 0;
4215
- if (!name) {
4216
- throw new JsonRpcError(-32602, "prompts/get requires params.name.");
4217
- }
4218
- const loaded = this.prompts.get(name);
4219
- if (!loaded) {
4220
- throw new JsonRpcError(-32602, `Unknown prompt "${name}".`);
4221
- }
4222
- const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
4223
- if (context.authSession !== void 0) {
4224
- toolContext.auth = context.authSession;
4225
- }
4226
- toolContext.notify = this.createNotifications(context, request);
4227
- return executePrompt(loaded.prompt, params?.arguments ?? {}, toPromptContext(toolContext));
4228
- }
4229
- /** Creates notification helpers scoped by advertised server capabilities. */
4230
- createNotifications(context, request) {
4231
- const configured = this.options.capabilities ?? {};
4232
- return createRuntimeNotifications(
4233
- context.notifications,
4234
- readProgressToken(request),
4235
- {
4236
- toolsListChanged: Boolean(configured.tools?.listChanged),
4237
- resourcesListChanged: Boolean(configured.resources?.listChanged),
4238
- promptsListChanged: Boolean(configured.prompts?.listChanged),
4239
- isResourceSubscribed: (uri) => this.subscribedResourceUris.has(uri)
4139
+ .topbar {
4140
+ align-items: center;
4141
+ background: color-mix(in srgb, var(--bg) 92%, var(--panel));
4142
+ border-bottom: 1px solid var(--border);
4143
+ display: flex;
4144
+ gap: 16px;
4145
+ justify-content: space-between;
4146
+ min-height: 64px;
4147
+ padding: 12px 18px;
4240
4148
  }
4241
- );
4242
- }
4243
- /** Accepts notifications without side effects for client compatibility. */
4244
- async handleNotification(request) {
4245
- if (request.method === "notifications/cancelled") {
4246
- this.cancelRequest(request);
4247
- }
4248
- }
4249
- /** Applies a client cancellation notification to an active request. */
4250
- cancelRequest(request) {
4251
- const params = request.params;
4252
- const requestId = params?.requestId;
4253
- if (typeof requestId !== "string" && typeof requestId !== "number") {
4254
- return;
4255
- }
4256
- const controller = this.activeRequests.get(requestId);
4257
- if (!controller || controller.signal.aborted) {
4258
- return;
4259
- }
4260
- const reason = typeof params?.reason === "string" ? params.reason : "Request cancelled.";
4261
- controller.abort(new RequestCancelledError(reason));
4262
- }
4263
- };
4264
- function createSidecarMcpServer(options) {
4265
- return new SidecarMcpServer(options);
4266
- }
4267
- function createSidecarHttpServer(options) {
4268
- return createServer(createSidecarHttpHandler(options));
4269
- }
4270
- function createSidecarHttpHandler(options) {
4271
- const endpoint = options.path ?? "/mcp";
4272
- const maxBodyBytes = options.maxBodyBytes ?? 1e6;
4273
- const streamHub = createSseHub();
4274
- const mcp = createSidecarMcpServer(options);
4275
- return async (request, response) => {
4276
- const pathname = request.url?.split("?")[0];
4277
- const requestLog = createHttpRequestLog(request, pathname);
4278
- response.once("finish", () => {
4279
- logHttpRequest(requestLog, response.statusCode);
4280
- });
4281
- if (isRejectedOrigin(request, options.allowedOrigins)) {
4282
- response.writeHead(403, { "content-type": "application/json" });
4283
- response.end(JSON.stringify({ error: "forbidden_origin" }));
4284
- return;
4285
- }
4286
- const proxyResult = await runProxy(options.proxy, request);
4287
- if (proxyResult) {
4288
- response.writeHead(proxyResult.status, proxyResult.headers);
4289
- response.end(proxyResult.body ?? "");
4290
- return;
4291
- }
4292
- if (options.auth && request.method === "GET" && isProtectedResourceMetadataPath(pathname, endpoint)) {
4293
- response.writeHead(200, { "content-type": "application/json" });
4294
- response.end(JSON.stringify(options.auth.metadata()));
4295
- return;
4296
- }
4297
- if (options.auth && request.method === "GET" && pathname === "/.well-known/oauth-authorization-server") {
4298
- await proxyAuthorizationServerMetadata(options.auth, response);
4299
- return;
4300
- }
4301
- if (request.method === "GET" && pathname === endpoint) {
4302
- try {
4303
- const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
4304
- if (options.auth) {
4305
- const authSession = await authorizeHttpRequest(options.auth, fetchRequest, response);
4306
- if (authSession === AUTH_RESPONSE_SENT) {
4307
- return;
4308
- }
4309
- }
4310
- validateProtocolVersion(request);
4311
- validateGetHeaders(request);
4312
- streamHub.open(response);
4313
- } catch (error) {
4314
- const status = error instanceof JsonRpcHttpError ? error.status : 400;
4315
- response.writeHead(status, { "content-type": "application/json" });
4316
- response.end(
4317
- JSON.stringify({
4318
- jsonrpc: JSONRPC_VERSION2,
4319
- id: null,
4320
- error: normalizeHttpError(error)
4321
- })
4322
- );
4149
+ .brand { display: grid; gap: 1px; min-width: 180px; }
4150
+ h1 { font-size: 15px; line-height: 1.1; margin: 0; }
4151
+ .subtitle { color: var(--muted); font-size: 12px; }
4152
+ .controls { align-items: center; display: flex; flex-wrap: wrap; gap: 14px; justify-content: flex-end; }
4153
+ .control-group { align-items: center; display: flex; }
4154
+ .segmented {
4155
+ background: var(--panel-soft);
4156
+ border: 1px solid var(--border);
4157
+ border-radius: 15px;
4158
+ display: grid;
4159
+ gap: 0;
4160
+ grid-template-columns: repeat(var(--segments, 2), 38px);
4161
+ padding: 3px;
4162
+ position: relative;
4323
4163
  }
4324
- return;
4325
- }
4326
- if (pathname === endpoint && request.method !== "POST") {
4327
- response.writeHead(405, {
4328
- "allow": "GET, POST",
4329
- "content-type": "application/json"
4330
- });
4331
- response.end(JSON.stringify({ error: "method_not_allowed" }));
4332
- return;
4333
- }
4334
- if (pathname !== endpoint) {
4335
- response.writeHead(404, { "content-type": "application/json" });
4336
- response.end(JSON.stringify({ error: "not_found" }));
4337
- return;
4338
- }
4339
- try {
4340
- const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
4341
- const authSession = options.auth ? await authorizeHttpRequest(options.auth, fetchRequest, response) : void 0;
4342
- if (options.auth && authSession === AUTH_RESPONSE_SENT) {
4343
- return;
4164
+ .segmented::before {
4165
+ background: var(--panel);
4166
+ border: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
4167
+ border-radius: 12px;
4168
+ box-shadow: 0 1px 8px rgb(0 0 0 / 16%);
4169
+ content: "";
4170
+ inset: 3px auto 3px 3px;
4171
+ position: absolute;
4172
+ transform: translateX(calc(var(--active-index, 0) * 38px));
4173
+ transition: transform 170ms cubic-bezier(.2, .8, .2, 1);
4174
+ width: 38px;
4344
4175
  }
4345
- validateProtocolVersion(request);
4346
- validatePostHeaders(request);
4347
- const body = await readJson(request, maxBodyBytes);
4348
- if (Array.isArray(body)) {
4349
- throw new JsonRpcHttpError(400, -32600, "JSON-RPC batches are not supported by MCP Streamable HTTP.");
4176
+ .segmented button, .send, .icon-button, .workspace-pill, .auth-trigger, .modal-button {
4177
+ appearance: none;
4178
+ cursor: pointer;
4179
+ font: inherit;
4350
4180
  }
4351
- if (isJsonRpcResponseMessage(body)) {
4352
- response.writeHead(202);
4353
- response.end();
4354
- return;
4181
+ .segmented button {
4182
+ align-items: center;
4183
+ background: transparent;
4184
+ border: 0;
4185
+ border-radius: 12px;
4186
+ color: var(--muted);
4187
+ display: inline-flex;
4188
+ font-weight: 650;
4189
+ justify-content: center;
4190
+ min-height: 38px;
4191
+ min-width: 38px;
4192
+ padding: 0;
4193
+ position: relative;
4194
+ transition: color 140ms ease;
4195
+ z-index: 1;
4355
4196
  }
4356
- const rpcRequest = assertJsonRpcRequest(body);
4357
- if (rpcRequest.id !== void 0 && hasProgressToken(rpcRequest)) {
4358
- const stream = createSseStream(response, { supportsRequestProgress: true });
4359
- const payload2 = await mcp.handle(rpcRequest, {
4360
- request: fetchRequest,
4361
- authSession,
4362
- notifications: stream
4363
- });
4364
- if (payload2) {
4365
- stream.sendJson(payload2);
4366
- }
4367
- stream.end();
4368
- return;
4197
+ .segmented button[aria-pressed="true"] {
4198
+ background: transparent;
4199
+ box-shadow: none;
4200
+ color: var(--text);
4369
4201
  }
4370
- const payload = await mcp.handle(rpcRequest, {
4371
- request: fetchRequest,
4372
- authSession,
4373
- notifications: streamHub
4374
- }) ?? null;
4375
- const responses = payload === null ? [] : [payload];
4376
- if (!responses.length) {
4377
- response.writeHead(202);
4378
- response.end();
4379
- return;
4202
+ .auth-trigger {
4203
+ align-items: center;
4204
+ background: var(--panel);
4205
+ border: 1px solid var(--border);
4206
+ border-radius: 13px;
4207
+ color: var(--text);
4208
+ display: inline-flex;
4209
+ font-weight: 700;
4210
+ min-height: 42px;
4211
+ padding: 0 18px;
4380
4212
  }
4381
- const authError = httpAuthError(payload);
4382
- response.writeHead(authError?.status ?? 200, {
4383
- "content-type": "application/json",
4384
- ...authError?.headers ?? {}
4385
- });
4386
- response.end(JSON.stringify(payload));
4387
- } catch (error) {
4388
- const status = error instanceof JsonRpcHttpError ? error.status : 400;
4389
- response.writeHead(status, { "content-type": "application/json" });
4390
- response.end(
4391
- JSON.stringify({
4392
- jsonrpc: JSONRPC_VERSION2,
4393
- id: null,
4394
- error: normalizeHttpError(error)
4395
- })
4396
- );
4397
- }
4398
- };
4399
- }
4400
- var DEFAULT_ALLOWED_ORIGINS = [
4401
- "https://chatgpt.com",
4402
- "https://chat.openai.com",
4403
- "https://claude.ai",
4404
- "https://*.claude.ai"
4405
- ];
4406
- function isRejectedOrigin(request, allowedOrigins = []) {
4407
- const origin = request.headers.origin;
4408
- if (!origin) {
4409
- return false;
4213
+ .auth-trigger:hover { background: color-mix(in srgb, var(--panel) 88%, var(--panel-soft)); }
4214
+ .auth-trigger:focus-visible, .modal-button:focus-visible, .modal-close:focus-visible {
4215
+ outline: 0;
4216
+ box-shadow: 0 0 0 3px var(--ring);
4217
+ }
4218
+ .icon {
4219
+ align-items: center;
4220
+ display: inline-flex;
4221
+ height: 17px;
4222
+ justify-content: center;
4223
+ line-height: 1;
4224
+ width: 17px;
4225
+ }
4226
+ .icon svg {
4227
+ display: block;
4228
+ height: 17px;
4229
+ stroke: currentColor;
4230
+ width: 17px;
4231
+ }
4232
+ .button-text { display: none; }
4233
+ .modal-input, textarea {
4234
+ background: var(--panel);
4235
+ border: 1px solid var(--border);
4236
+ border-radius: 10px;
4237
+ color: var(--text);
4238
+ font: inherit;
4239
+ outline: 0;
4240
+ }
4241
+ .modal-input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
4242
+ .modal-input { min-height: 40px; padding: 0 11px; width: 100%; }
4243
+ .modal-overlay {
4244
+ align-items: center;
4245
+ background: rgb(0 0 0 / 42%);
4246
+ display: flex;
4247
+ inset: 0;
4248
+ justify-content: center;
4249
+ opacity: 0;
4250
+ padding: 20px;
4251
+ pointer-events: none;
4252
+ position: fixed;
4253
+ transition: opacity 150ms ease;
4254
+ z-index: 30;
4255
+ }
4256
+ .modal-overlay[data-open="true"] {
4257
+ opacity: 1;
4258
+ pointer-events: auto;
4259
+ }
4260
+ .modal-panel {
4261
+ background: var(--panel);
4262
+ border: 1px solid var(--border);
4263
+ border-radius: 12px;
4264
+ box-shadow: var(--shadow);
4265
+ display: grid;
4266
+ gap: 16px;
4267
+ max-width: min(440px, calc(100vw - 32px));
4268
+ opacity: 0;
4269
+ padding: 20px;
4270
+ transform: translateY(10px) scale(.98);
4271
+ transition: opacity 150ms ease, transform 150ms ease;
4272
+ width: 100%;
4273
+ }
4274
+ .modal-overlay[data-open="true"] .modal-panel {
4275
+ opacity: 1;
4276
+ transform: translateY(0) scale(1);
4277
+ }
4278
+ .modal-head {
4279
+ align-items: start;
4280
+ display: flex;
4281
+ gap: 16px;
4282
+ justify-content: space-between;
4283
+ }
4284
+ .modal-title { font-size: 17px; font-weight: 800; margin: 0; }
4285
+ .modal-description { color: var(--muted); font-size: 13px; margin: 6px 0 0; }
4286
+ .modal-close {
4287
+ align-items: center;
4288
+ appearance: none;
4289
+ background: transparent;
4290
+ border: 0;
4291
+ border-radius: 999px;
4292
+ color: var(--muted);
4293
+ cursor: pointer;
4294
+ display: inline-flex;
4295
+ height: 28px;
4296
+ justify-content: center;
4297
+ padding: 0;
4298
+ width: 28px;
4299
+ }
4300
+ .modal-close svg {
4301
+ display: block;
4302
+ height: 16px;
4303
+ width: 16px;
4304
+ }
4305
+ .modal-field { display: grid; gap: 7px; }
4306
+ .modal-label { font-size: 13px; font-weight: 700; }
4307
+ .modal-error { color: #c43b32; display: none; font-size: 13px; }
4308
+ .modal-error[data-visible="true"] { display: block; }
4309
+ .modal-actions {
4310
+ display: flex;
4311
+ gap: 10px;
4312
+ justify-content: flex-end;
4313
+ }
4314
+ .modal-button {
4315
+ border-radius: 9px;
4316
+ font-weight: 750;
4317
+ min-height: 38px;
4318
+ padding: 0 13px;
4319
+ }
4320
+ .modal-button[data-variant="secondary"] {
4321
+ background: transparent;
4322
+ border: 1px solid var(--border);
4323
+ color: var(--text);
4324
+ }
4325
+ .modal-button[data-variant="primary"] {
4326
+ background: var(--control-bg);
4327
+ border: 1px solid var(--control-bg);
4328
+ color: var(--control-text);
4329
+ }
4330
+ .modal-button:disabled {
4331
+ cursor: wait;
4332
+ opacity: .68;
4333
+ }
4334
+ .stage {
4335
+ display: flex;
4336
+ flex: 1 1 auto;
4337
+ min-height: 0;
4338
+ overflow: hidden;
4339
+ }
4340
+ .surface {
4341
+ background: var(--bg);
4342
+ box-shadow: none;
4343
+ display: flex;
4344
+ flex-direction: column;
4345
+ height: 100%;
4346
+ min-height: 0;
4347
+ overflow: hidden;
4348
+ transition: width 160ms ease, height 160ms ease, border-radius 160ms ease, box-shadow 160ms ease;
4349
+ width: 100%;
4350
+ }
4351
+ :root[data-sidecar-device="mobile"] .surface {
4352
+ border: 1px solid var(--border);
4353
+ border-radius: 28px;
4354
+ box-shadow: var(--shadow);
4355
+ height: min(820px, calc(100dvh - 112px));
4356
+ width: var(--mobile-width);
4357
+ }
4358
+ :root[data-sidecar-device="mobile"] .stage {
4359
+ align-items: center;
4360
+ display: flex;
4361
+ justify-content: center;
4362
+ padding: 18px;
4363
+ }
4364
+ .surface-bar {
4365
+ align-items: center;
4366
+ background: var(--bg);
4367
+ border-bottom: 1px solid var(--border);
4368
+ display: flex;
4369
+ gap: 10px;
4370
+ justify-content: space-between;
4371
+ min-height: 50px;
4372
+ padding: 0 16px;
4373
+ }
4374
+ .surface-title { align-items: center; display: flex; gap: 8px; font-weight: 760; }
4375
+ .surface-dot {
4376
+ background: var(--accent);
4377
+ border-radius: 999px;
4378
+ box-shadow: 0 0 0 4px var(--ring);
4379
+ height: 8px;
4380
+ width: 8px;
4381
+ }
4382
+ .surface-meta { color: var(--muted); font-size: 12px; }
4383
+ .messages {
4384
+ display: flex;
4385
+ flex-direction: column;
4386
+ flex: 1 1 auto;
4387
+ gap: 14px;
4388
+ min-height: 0;
4389
+ overflow: auto;
4390
+ padding: 18px 16px;
4391
+ width: 100%;
4392
+ }
4393
+ .message {
4394
+ display: grid;
4395
+ gap: 8px;
4396
+ }
4397
+ .message[data-role="user"] {
4398
+ background: color-mix(in srgb, var(--accent) 13%, transparent);
4399
+ border-radius: 18px 18px 4px 18px;
4400
+ color: var(--text);
4401
+ justify-self: end;
4402
+ margin-left: auto;
4403
+ max-width: 85%;
4404
+ padding: 9px 12px;
4405
+ }
4406
+ :root[data-sidecar-device="mobile"] .message[data-role="user"] { max-width: 92%; }
4407
+ .message[data-role="assistant"], .tool-card { width: 100%; }
4408
+ .role { display: none; }
4409
+ .tool-title {
4410
+ color: var(--muted);
4411
+ font-size: 11px;
4412
+ font-weight: 800;
4413
+ letter-spacing: .05em;
4414
+ text-transform: uppercase;
4415
+ }
4416
+ .content { font-size: 14px; overflow-wrap: anywhere; white-space: pre-wrap; }
4417
+ .content.markdown { display: grid; gap: 10px; white-space: normal; }
4418
+ .content.markdown > * { margin-block: 0; }
4419
+ .content.markdown p { margin: 0; }
4420
+ .content.markdown ul, .content.markdown ol { margin: 0; padding-left: 20px; }
4421
+ .content.markdown pre {
4422
+ background: color-mix(in srgb, var(--panel-soft) 78%, black);
4423
+ border: 1px solid var(--border);
4424
+ border-radius: 10px;
4425
+ overflow: auto;
4426
+ padding: 10px;
4427
+ }
4428
+ .content.markdown code {
4429
+ background: var(--panel-soft);
4430
+ border-radius: 5px;
4431
+ font-size: .93em;
4432
+ padding: 1px 4px;
4433
+ }
4434
+ .content.markdown pre code { background: transparent; padding: 0; }
4435
+ .content.markdown table {
4436
+ border-collapse: collapse;
4437
+ display: block;
4438
+ max-width: 100%;
4439
+ overflow-x: auto;
4440
+ }
4441
+ .content.markdown th, .content.markdown td {
4442
+ border: 1px solid var(--border);
4443
+ padding: 6px 8px;
4444
+ text-align: left;
4445
+ }
4446
+ .tool-card {
4447
+ background: transparent;
4448
+ border: 1px solid var(--border);
4449
+ border-radius: 12px;
4450
+ overflow: hidden;
4451
+ }
4452
+ .tool-head {
4453
+ align-items: center;
4454
+ background: color-mix(in srgb, var(--panel-soft) 58%, transparent);
4455
+ border-bottom: 1px solid var(--border);
4456
+ display: flex;
4457
+ justify-content: space-between;
4458
+ padding: 7px 10px;
4459
+ }
4460
+ .tool-body { display: grid; gap: 10px; padding: 12px; }
4461
+ iframe {
4462
+ background: var(--widget-bg);
4463
+ border: 1px solid var(--border);
4464
+ border-radius: 14px;
4465
+ min-height: 380px;
4466
+ width: 100%;
4467
+ }
4468
+ :root[data-sidecar-device="mobile"] iframe { min-height: 520px; }
4469
+ .composer {
4470
+ background: var(--bg);
4471
+ padding: 10px 16px 14px;
4472
+ flex: 0 0 auto;
4473
+ }
4474
+ form {
4475
+ align-items: end;
4476
+ display: flex;
4477
+ gap: 8px;
4478
+ }
4479
+ textarea {
4480
+ background: color-mix(in srgb, var(--panel-soft) 35%, transparent);
4481
+ border: 0;
4482
+ min-height: 56px;
4483
+ max-height: 124px;
4484
+ padding: 10px 44px 10px 12px;
4485
+ resize: none;
4486
+ width: 100%;
4487
+ }
4488
+ .send {
4489
+ align-items: center;
4490
+ aspect-ratio: 1;
4491
+ align-self: center;
4492
+ background: var(--send-bg);
4493
+ border-radius: 999px;
4494
+ color: var(--send-text);
4495
+ display: inline-flex;
4496
+ height: 36px;
4497
+ justify-content: center;
4498
+ margin-left: -52px;
4499
+ min-height: 36px;
4500
+ padding: 0;
4501
+ width: 36px;
4502
+ }
4503
+ .send:hover { filter: brightness(.96); }
4504
+ :root[data-sidecar-theme="dark"] .send:hover { filter: brightness(1.08); }
4505
+ .send svg {
4506
+ display: block;
4507
+ height: 18px;
4508
+ stroke-width: 2.4;
4509
+ width: 18px;
4510
+ }
4511
+ .status { color: var(--muted); font-size: 12px; }
4512
+ .error { color: #c43b32; }
4513
+ .empty {
4514
+ align-self: center;
4515
+ color: var(--muted);
4516
+ display: grid;
4517
+ gap: 8px;
4518
+ justify-self: center;
4519
+ margin: auto 0;
4520
+ padding: 24px 12px;
4521
+ text-align: center;
4522
+ }
4523
+ .empty strong { color: var(--text); font-size: 16px; }
4524
+ pre {
4525
+ background: var(--panel-soft);
4526
+ border-radius: 12px;
4527
+ margin: 0;
4528
+ overflow: auto;
4529
+ padding: 12px;
4530
+ }
4531
+ @media (max-width: 760px) {
4532
+ .shell { display: flex; flex-direction: column; }
4533
+ .topbar { align-items: stretch; flex-direction: column; min-height: 0; padding: 12px; }
4534
+ .brand { min-width: 0; }
4535
+ .controls { justify-content: flex-start; }
4536
+ .auth-trigger { min-height: 38px; }
4537
+ .stage { display: flex; justify-content: center; padding: 12px; }
4538
+ .surface { border: 1px solid var(--border); border-radius: 20px; box-shadow: var(--shadow); width: calc(100vw - 24px); }
4539
+ .send { flex: 0 0 32px; }
4540
+ }
4541
+ </style>
4542
+ </head>
4543
+ <body>
4544
+ <main class="shell">
4545
+ <section class="content-shell">
4546
+ <div class="topbar">
4547
+ <div class="brand">
4548
+ <h1>Sidecar dev</h1>
4549
+ <div class="subtitle">Local MCP app simulator</div>
4550
+ </div>
4551
+ <div class="controls">
4552
+ <div class="control-group">
4553
+ <div class="segmented" aria-label="Host">
4554
+ <button type="button" data-host="chatgpt" aria-label="ChatGPT preview" title="ChatGPT"><span class="icon">${iconSvg("chatgpt")}</span><span class="button-text">ChatGPT</span></button>
4555
+ <button type="button" data-host="claude" aria-label="Claude preview" title="Claude"><span class="icon">${iconSvg("claude")}</span><span class="button-text">Claude</span></button>
4556
+ <button type="button" data-host="generic" aria-label="Generic MCP preview" title="MCP"><span class="icon">${iconSvg("braces")}</span><span class="button-text">MCP</span></button>
4557
+ </div>
4558
+ </div>
4559
+ <div class="control-group">
4560
+ <div class="segmented" aria-label="Theme">
4561
+ <button type="button" data-theme="light" aria-label="Light theme" title="Light"><span class="icon">${iconSvg("sun")}</span><span class="button-text">Light</span></button>
4562
+ <button type="button" data-theme="dark" aria-label="Dark theme" title="Dark"><span class="icon">${iconSvg("moon")}</span><span class="button-text">Dark</span></button>
4563
+ </div>
4564
+ </div>
4565
+ <div class="control-group">
4566
+ <div class="segmented" aria-label="Device">
4567
+ <button type="button" data-device="desktop" aria-label="Desktop preview" title="Desktop"><span class="icon">${iconSvg("monitor")}</span><span class="button-text">Desktop</span></button>
4568
+ <button type="button" data-device="mobile" aria-label="Mobile preview" title="Mobile"><span class="icon">${iconSvg("phone")}</span><span class="button-text">Mobile</span></button>
4569
+ </div>
4570
+ </div>
4571
+ <button id="authTrigger" class="auth-trigger" type="button" data-token-set="false">
4572
+ <span>Set Bearer Token</span>
4573
+ </button>
4574
+ </div>
4575
+ </div>
4576
+ <section class="stage">
4577
+ <aside class="surface" aria-label="Sidecar chat preview">
4578
+ <div class="surface-bar">
4579
+ <div class="surface-title"><span class="surface-dot"></span><span id="surfaceTitle">Claude preview</span></div>
4580
+ <div id="surfaceMeta" class="surface-meta">Desktop</div>
4581
+ </div>
4582
+ <section id="messages" class="messages">
4583
+ <div class="empty"><strong>Start a local MCP run.</strong><span>Ask for a tool call, then inspect model text and widget output here.</span></div>
4584
+ </section>
4585
+ <section class="composer">
4586
+ <form id="chatForm">
4587
+ <textarea id="prompt" placeholder="Ask a question or request a tool call..."></textarea>
4588
+ <button class="send" type="submit" aria-label="Send">
4589
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
4590
+ <path d="M12 19V5" />
4591
+ <path d="m5 12 7-7 7 7" />
4592
+ </svg>
4593
+ </button>
4594
+ </form>
4595
+ <div id="status" class="status"></div>
4596
+ </section>
4597
+ </aside>
4598
+ </section>
4599
+ </section>
4600
+ </main>
4601
+ <div id="authModal" class="modal-overlay" data-open="false" aria-hidden="true">
4602
+ <section class="modal-panel" role="dialog" aria-modal="true" aria-labelledby="authModalTitle" aria-describedby="authModalDescription">
4603
+ <div class="modal-head">
4604
+ <div>
4605
+ <h2 id="authModalTitle" class="modal-title">Set bearer token</h2>
4606
+ <p id="authModalDescription" class="modal-description">Sidecar will test this token against the local MCP server before saving it.</p>
4607
+ </div>
4608
+ <button id="authClose" class="modal-close" type="button" aria-label="Close bearer token dialog">${iconSvg("x")}</button>
4609
+ </div>
4610
+ <label class="modal-field" for="authToken">
4611
+ <span class="modal-label">Bearer token</span>
4612
+ <input id="authToken" class="modal-input" type="password" autocomplete="off" placeholder="Paste a bearer token" />
4613
+ </label>
4614
+ <div id="authError" class="modal-error" role="status"></div>
4615
+ <div class="modal-actions">
4616
+ <button id="authCancel" class="modal-button" data-variant="secondary" type="button">Cancel</button>
4617
+ <button id="authSave" class="modal-button" data-variant="primary" type="button">Save</button>
4618
+ </div>
4619
+ </section>
4620
+ </div>
4621
+ <script>
4622
+ ${devHarnessBrowserScript(options.initialBearerToken)}
4623
+ </script>
4624
+ </body>
4625
+ </html>`;
4626
+ }
4627
+ var lobeIconCache = /* @__PURE__ */ new Map();
4628
+ function iconSvg(name) {
4629
+ const common = `viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"`;
4630
+ switch (name) {
4631
+ case "chatgpt":
4632
+ return lobeIconSvg("openai.svg") ?? `<svg ${common}><path d="M7.5 8.5a5 5 0 0 1 8.6-3.5 4.2 4.2 0 0 1 5.1 5.1 5 5 0 0 1-3.6 8.5 4.2 4.2 0 0 1-6.8 2.2 5 5 0 0 1-8-4.3 4.2 4.2 0 0 1-1-6.9 5 5 0 0 1 5.7-1.1Z"/></svg>`;
4633
+ case "claude":
4634
+ return lobeIconSvg("claude.svg") ?? `<svg ${common}><path d="M12 3v18"/><path d="m5.6 5.6 12.8 12.8"/><path d="M3 12h18"/><path d="M5.6 18.4 18.4 5.6"/></svg>`;
4635
+ case "braces":
4636
+ return `<svg ${common}><path d="M8 4c-2 1.3-3 2.8-3 4.7v1.1c0 1-.7 1.8-1.7 2.2C4.3 12.4 5 13.2 5 14.2v1.1c0 1.9 1 3.4 3 4.7"/><path d="M16 4c2 1.3 3 2.8 3 4.7v1.1c0 1 .7 1.8 1.7 2.2-1 .4-1.7 1.2-1.7 2.2v1.1c0 1.9-1 3.4-3 4.7"/></svg>`;
4637
+ case "sun":
4638
+ return `<svg ${common}><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>`;
4639
+ case "moon":
4640
+ return `<svg ${common}><path d="M20.5 14.5A8.5 8.5 0 0 1 9.5 3.5 7 7 0 1 0 20.5 14.5Z"/></svg>`;
4641
+ case "monitor":
4642
+ return `<svg ${common}><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8"/><path d="M12 16v4"/></svg>`;
4643
+ case "phone":
4644
+ return `<svg ${common}><rect x="7" y="2.5" width="10" height="19" rx="2"/><path d="M11 18.5h2"/></svg>`;
4645
+ case "x":
4646
+ return `<svg ${common}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
4647
+ }
4648
+ }
4649
+ function lobeIconSvg(fileName) {
4650
+ const cached = lobeIconCache.get(fileName);
4651
+ if (cached) {
4652
+ return cached;
4410
4653
  }
4411
- const host = request.headers.host ?? "";
4412
- let originUrl;
4413
4654
  try {
4414
- originUrl = new URL(origin);
4655
+ const file = require2.resolve(`@lobehub/icons-static-svg/icons/${fileName}`);
4656
+ const svg = readFileSync(file, "utf8").replace(/\s+xmlns="[^"]*"/, "").replace("<svg ", '<svg aria-hidden="true" ');
4657
+ lobeIconCache.set(fileName, svg);
4658
+ return svg;
4415
4659
  } catch {
4416
- return true;
4417
- }
4418
- if (isLocalHost(host)) {
4419
- return !isLocalHost(originUrl.host);
4420
- }
4421
- const requestOrigin = `https://${host}`;
4422
- const allowed = [requestOrigin, ...DEFAULT_ALLOWED_ORIGINS, ...allowedOrigins];
4423
- return !allowed.some((pattern) => matchesOrigin(pattern, originUrl.origin));
4424
- }
4425
- function isLocalHost(host) {
4426
- if (host.startsWith("[::1]")) {
4427
- return true;
4428
- }
4429
- const hostname = host.split(":")[0]?.toLowerCase();
4430
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
4431
- }
4432
- function matchesOrigin(pattern, origin) {
4433
- if (pattern === origin) {
4434
- return true;
4435
- }
4436
- if (!pattern.includes("*")) {
4437
- return false;
4438
- }
4439
- const escaped = pattern.split("*").map(escapeRegExp).join("[^.]+");
4440
- return new RegExp(`^${escaped}$`).test(origin);
4441
- }
4442
- function escapeRegExp(value) {
4443
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4444
- }
4445
- function isProtectedResourceMetadataPath(pathname, endpoint) {
4446
- return pathname === "/.well-known/oauth-protected-resource" || pathname === `/.well-known/oauth-protected-resource${endpoint}`;
4447
- }
4448
- function createHttpRequestLog(request, pathname) {
4449
- return {
4450
- method: request.method,
4451
- path: pathname,
4452
- host: truncateHeader(singleHeader(request.headers.host)),
4453
- accept: truncateHeader(singleHeader(request.headers.accept)),
4454
- contentType: truncateHeader(singleHeader(request.headers["content-type"])),
4455
- contentLength: truncateHeader(singleHeader(request.headers["content-length"])),
4456
- mcpProtocolVersion: truncateHeader(singleHeader(request.headers["mcp-protocol-version"])),
4457
- origin: truncateHeader(singleHeader(request.headers.origin)),
4458
- userAgent: truncateHeader(singleHeader(request.headers["user-agent"])),
4459
- authorization: request.headers.authorization ? "present" : "absent",
4460
- cookie: request.headers.cookie ? "present" : "absent"
4461
- };
4462
- }
4463
- function logHttpRequest(metadata, status) {
4464
- const debug = process.env.SIDECAR_DEBUG === "1" || process.env.SIDECAR_LOG_LEVEL === "debug";
4465
- if (!debug && status < 400) {
4466
- return;
4467
- }
4468
- const message = JSON.stringify({
4469
- event: "sidecar.mcp.http",
4470
- status,
4471
- ...stripUndefined4(metadata)
4472
- });
4473
- if (status >= 500) {
4474
- console.error(message);
4475
- } else if (status >= 400) {
4476
- console.warn(message);
4477
- } else {
4478
- console.info(message);
4479
- }
4480
- }
4481
- function singleHeader(value) {
4482
- return Array.isArray(value) ? value.join(", ") : value;
4483
- }
4484
- function truncateHeader(value) {
4485
- if (!value) {
4486
4660
  return void 0;
4487
4661
  }
4488
- return value.length > 240 ? `${value.slice(0, 237)}...` : value;
4489
4662
  }
4490
- async function proxyAuthorizationServerMetadata(auth, response) {
4491
- const [authorizationServer] = auth.authorizationServers;
4492
- if (!authorizationServer) {
4493
- response.writeHead(404, { "content-type": "application/json" });
4494
- response.end(JSON.stringify({ error: "authorization_server_not_configured" }));
4663
+ async function handleChatRequest(request, response, options) {
4664
+ const body = await readJson(request);
4665
+ const messages = readChatMessages(body.messages);
4666
+ const authToken = readAuthToken(body.authToken);
4667
+ const apiKey = options.openAiApiKey ?? process.env.OPENAI_API_KEY;
4668
+ response.writeHead(200, {
4669
+ "cache-control": "no-cache, no-transform",
4670
+ "connection": "keep-alive",
4671
+ "content-type": "text/event-stream; charset=utf-8",
4672
+ "x-accel-buffering": "no"
4673
+ });
4674
+ const sink = createSseSink(response);
4675
+ if (!apiKey) {
4676
+ sink.send("error", {
4677
+ message: "OPENAI_API_KEY is required for sidecar dev chat."
4678
+ });
4679
+ response.end();
4495
4680
  return;
4496
4681
  }
4497
4682
  try {
4498
- const url = new URL("/.well-known/oauth-authorization-server", authorizationServer);
4499
- const upstream = await fetch(url, { headers: { accept: "application/json" } });
4500
- const body = await upstream.text();
4501
- response.writeHead(upstream.ok ? 200 : 502, {
4502
- "content-type": upstream.headers.get("content-type") ?? "application/json"
4683
+ const mcpTools = await listAllMcpTools(options.mcpUrl, authToken);
4684
+ const toolMap = mcpToolsToOpenAiTools(mcpTools);
4685
+ await streamChatWithTools({
4686
+ apiKey,
4687
+ baseUrl: options.openAiBaseUrl ?? process.env.OPENAI_BASE_URL ?? OPENAI_CHAT_COMPLETIONS_URL,
4688
+ model: options.model,
4689
+ messages: toOpenAiMessages(messages),
4690
+ tools: toolMap,
4691
+ mcpUrl: options.mcpUrl,
4692
+ authToken,
4693
+ sink
4503
4694
  });
4504
- response.end(body);
4695
+ sink.send("done", {});
4505
4696
  } catch (error) {
4506
- console.warn(JSON.stringify({
4507
- event: "sidecar.mcp.authorization_metadata_proxy_failed",
4508
- authorizationServer,
4509
- message: error instanceof Error ? error.message : "Unknown error"
4510
- }));
4511
- response.writeHead(502, { "content-type": "application/json" });
4512
- response.end(JSON.stringify({ error: "authorization_metadata_unavailable" }));
4697
+ sink.send("error", normalizeHttpError(error));
4698
+ } finally {
4699
+ response.end();
4700
+ }
4701
+ }
4702
+ async function streamChatWithTools(options) {
4703
+ const messages = [...options.messages];
4704
+ for (let iteration = 0; iteration < MAX_CHAT_TOOL_ITERATIONS; iteration += 1) {
4705
+ const assistant = await streamOpenAiChat({
4706
+ apiKey: options.apiKey,
4707
+ baseUrl: options.baseUrl,
4708
+ model: options.model,
4709
+ messages,
4710
+ tools: options.tools.tools,
4711
+ sink: options.sink
4712
+ });
4713
+ messages.push(assistant.message);
4714
+ if (!assistant.toolCalls.length) {
4715
+ return;
4716
+ }
4717
+ for (const toolCall of assistant.toolCalls) {
4718
+ const descriptor = options.tools.byOpenAiName.get(toolCall.function.name);
4719
+ if (!descriptor) {
4720
+ throw new HttpError(500, `Unknown model tool call "${toolCall.function.name}".`);
4721
+ }
4722
+ const args = parseToolArguments(toolCall.function.arguments);
4723
+ options.sink.send("tool_start", {
4724
+ id: toolCall.id,
4725
+ name: descriptor.name,
4726
+ title: descriptor.title ?? descriptor.name,
4727
+ arguments: args
4728
+ });
4729
+ const result = await callMcp(
4730
+ options.mcpUrl,
4731
+ "tools/call",
4732
+ { name: descriptor.name, arguments: args },
4733
+ options.authToken
4734
+ );
4735
+ options.sink.send("tool_result", {
4736
+ id: toolCall.id,
4737
+ tool: {
4738
+ name: descriptor.name,
4739
+ title: descriptor.title ?? descriptor.name,
4740
+ resourceUri: toolResourceUri(descriptor)
4741
+ },
4742
+ result
4743
+ });
4744
+ messages.push({
4745
+ role: "tool",
4746
+ tool_call_id: toolCall.id,
4747
+ content: toolResultForModel(result)
4748
+ });
4749
+ }
4513
4750
  }
4751
+ throw new HttpError(500, "The model kept requesting tools and hit the dev harness iteration limit.");
4514
4752
  }
4515
- var JsonRpcError = class extends Error {
4516
- constructor(code, message, data) {
4517
- super(message);
4518
- this.code = code;
4519
- this.data = data;
4520
- this.name = "JsonRpcError";
4521
- }
4522
- code;
4523
- data;
4524
- };
4525
- var JsonRpcHttpError = class extends JsonRpcError {
4526
- constructor(status, code, message, data) {
4527
- super(code, message, data);
4528
- this.status = status;
4529
- this.name = "JsonRpcHttpError";
4530
- }
4531
- status;
4532
- };
4533
- var RequestCancelledError = class extends Error {
4534
- constructor(message = "Request cancelled.") {
4535
- super(message);
4536
- this.name = "RequestCancelledError";
4537
- }
4538
- };
4539
- var AUTH_RESPONSE_SENT = /* @__PURE__ */ Symbol("sidecar.auth.response-sent");
4540
- async function authorizeHttpRequest(auth, request, response) {
4541
- const result = await auth.authorizeRequest(request);
4542
- if (result.ok) {
4543
- return result.auth;
4544
- }
4545
- response.writeHead(result.status, {
4546
- "content-type": "application/json",
4547
- ...headersToRecord(result.headers)
4753
+ async function streamOpenAiChat(options) {
4754
+ const response = await fetch(options.baseUrl, {
4755
+ method: "POST",
4756
+ headers: {
4757
+ "authorization": `Bearer ${options.apiKey}`,
4758
+ "content-type": "application/json"
4759
+ },
4760
+ body: JSON.stringify({
4761
+ model: options.model,
4762
+ messages: options.messages,
4763
+ stream: true,
4764
+ tools: options.tools,
4765
+ tool_choice: "auto"
4766
+ })
4548
4767
  });
4549
- response.end(
4550
- JSON.stringify({
4551
- jsonrpc: JSONRPC_VERSION2,
4552
- id: null,
4553
- error: {
4554
- code: result.status === 401 ? -32001 : -32003,
4555
- message: result.body.error_description ?? result.body.error,
4556
- data: {
4557
- status: result.status,
4558
- body: result.body
4768
+ if (!response.ok || !response.body) {
4769
+ throw new HttpError(
4770
+ response.status,
4771
+ `OpenAI chat request failed with HTTP ${response.status}: ${await response.text()}`
4772
+ );
4773
+ }
4774
+ let content = "";
4775
+ const toolCalls = /* @__PURE__ */ new Map();
4776
+ for await (const event of readOpenAiSse(response.body)) {
4777
+ if (event === "[DONE]") {
4778
+ break;
4779
+ }
4780
+ const parsed = JSON.parse(event);
4781
+ const delta = parsed.choices?.[0]?.delta;
4782
+ if (!delta) {
4783
+ continue;
4784
+ }
4785
+ if (delta.content) {
4786
+ content += delta.content;
4787
+ options.sink.send("delta", { text: delta.content });
4788
+ }
4789
+ for (const chunk of delta.tool_calls ?? []) {
4790
+ const current = toolCalls.get(chunk.index) ?? {
4791
+ id: chunk.id ?? `call_${chunk.index}`,
4792
+ type: "function",
4793
+ function: {
4794
+ name: "",
4795
+ arguments: ""
4559
4796
  }
4560
- }
4561
- })
4562
- );
4563
- return AUTH_RESPONSE_SENT;
4564
- }
4565
- function createDefaultContext(request) {
4566
- const memory = /* @__PURE__ */ new Map();
4797
+ };
4798
+ current.id = chunk.id ?? current.id;
4799
+ current.type = chunk.type ?? current.type;
4800
+ current.function.name += chunk.function?.name ?? "";
4801
+ current.function.arguments += chunk.function?.arguments ?? "";
4802
+ toolCalls.set(chunk.index, current);
4803
+ }
4804
+ }
4805
+ const calls = [...toolCalls.entries()].sort(([left], [right]) => left - right).map(([, call]) => call);
4567
4806
  return {
4568
- auth: void 0,
4569
- request: {
4570
- id: String(request.id ?? randomUUID2()),
4571
- signal: new AbortController().signal,
4572
- host: "unknown",
4573
- transport: "streamable-http"
4574
- },
4575
- services: {},
4576
- tools: {},
4577
- log: consoleLogger,
4578
- trace: {
4579
- async span(_name, run) {
4580
- return run();
4581
- }
4582
- },
4583
- storage: {
4584
- async get(key) {
4585
- return memory.get(key);
4586
- },
4587
- async set(key, value) {
4588
- memory.set(key, value);
4589
- },
4590
- async delete(key) {
4591
- memory.delete(key);
4592
- }
4807
+ message: {
4808
+ role: "assistant",
4809
+ content: content || null,
4810
+ ...calls.length ? { tool_calls: calls } : {}
4593
4811
  },
4594
- notify: noopNotifications,
4595
- env: process.env
4812
+ toolCalls: calls
4596
4813
  };
4597
4814
  }
4598
- var noopNotifications = {
4599
- async progress() {
4600
- },
4601
- async toolsChanged() {
4602
- },
4603
- async resourcesChanged() {
4604
- },
4605
- async promptsChanged() {
4606
- },
4607
- async resourceUpdated() {
4608
- }
4609
- };
4610
- function createRuntimeNotifications(sink, progressToken, options) {
4611
- return {
4612
- async progress(update) {
4613
- if (!sink?.supportsRequestProgress || progressToken === void 0) {
4614
- return;
4615
- }
4616
- sink.send("notifications/progress", stripUndefined4({
4617
- progressToken,
4618
- progress: update.progress,
4619
- total: update.total,
4620
- message: update.message
4621
- }));
4622
- },
4623
- async toolsChanged() {
4624
- if (options.toolsListChanged) {
4625
- sink?.send("notifications/tools/list_changed");
4626
- }
4627
- },
4628
- async resourcesChanged() {
4629
- if (options.resourcesListChanged) {
4630
- sink?.send("notifications/resources/list_changed");
4631
- }
4632
- },
4633
- async promptsChanged() {
4634
- if (options.promptsListChanged) {
4635
- sink?.send("notifications/prompts/list_changed");
4815
+ async function* readOpenAiSse(body) {
4816
+ const decoder = new TextDecoder();
4817
+ let buffer = "";
4818
+ const reader = body.getReader();
4819
+ while (true) {
4820
+ const { done, value } = await reader.read();
4821
+ if (done) {
4822
+ break;
4823
+ }
4824
+ const chunk = value;
4825
+ buffer += decoder.decode(chunk, { stream: true });
4826
+ let boundary = buffer.indexOf("\n\n");
4827
+ while (boundary !== -1) {
4828
+ const raw = buffer.slice(0, boundary);
4829
+ buffer = buffer.slice(boundary + 2);
4830
+ for (const line of raw.split(/\r?\n/)) {
4831
+ if (line.startsWith("data:")) {
4832
+ yield line.slice(5).trim();
4833
+ }
4636
4834
  }
4637
- },
4638
- async resourceUpdated(uri) {
4639
- if (options.isResourceSubscribed(uri)) {
4640
- sink?.send("notifications/resources/updated", { uri });
4835
+ boundary = buffer.indexOf("\n\n");
4836
+ }
4837
+ }
4838
+ buffer += decoder.decode();
4839
+ if (buffer.trim()) {
4840
+ for (const line of buffer.split(/\r?\n/)) {
4841
+ if (line.startsWith("data:")) {
4842
+ yield line.slice(5).trim();
4641
4843
  }
4642
4844
  }
4643
- };
4644
- }
4645
- function toResourceContext(ctx) {
4646
- return {
4647
- auth: ctx.auth,
4648
- request: ctx.request,
4649
- services: ctx.services,
4650
- log: ctx.log,
4651
- storage: ctx.storage,
4652
- notify: ctx.notify,
4653
- env: ctx.env
4654
- };
4845
+ }
4655
4846
  }
4656
- function toPromptContext(ctx) {
4847
+ async function listAllMcpTools(mcpUrl, authToken) {
4848
+ const tools = [];
4849
+ let cursor;
4850
+ do {
4851
+ const result = await callMcp(
4852
+ mcpUrl,
4853
+ "tools/list",
4854
+ cursor ? { cursor } : {},
4855
+ authToken
4856
+ );
4857
+ tools.push(...result.tools ?? []);
4858
+ cursor = result.nextCursor;
4859
+ } while (cursor);
4860
+ return tools;
4861
+ }
4862
+ async function readMcpResource(mcpUrl, uri, authToken) {
4863
+ const result = await callMcp(mcpUrl, "resources/read", { uri }, authToken);
4864
+ const content = result.contents?.find((entry) => typeof entry.text === "string") ?? result.contents?.[0];
4657
4865
  return {
4658
- auth: ctx.auth,
4659
- request: ctx.request,
4660
- services: ctx.services,
4661
- log: ctx.log,
4662
- storage: ctx.storage,
4663
- notify: ctx.notify,
4664
- env: ctx.env
4866
+ text: content?.text ?? "",
4867
+ mimeType: content?.mimeType
4665
4868
  };
4666
4869
  }
4667
- function readCursorParam(request) {
4668
- const params = request.params;
4669
- if (params?.cursor === void 0) {
4670
- return void 0;
4870
+ async function callMcp(mcpUrl, method, params, authToken) {
4871
+ const response = await fetch(mcpUrl, {
4872
+ method: "POST",
4873
+ headers: {
4874
+ "accept": "application/json, text/event-stream",
4875
+ "content-type": "application/json",
4876
+ "mcp-protocol-version": MCP_PROTOCOL_VERSION,
4877
+ ...authToken ? { authorization: `Bearer ${authToken}` } : {}
4878
+ },
4879
+ body: JSON.stringify({
4880
+ jsonrpc: "2.0",
4881
+ id: randomUUID3(),
4882
+ method,
4883
+ params
4884
+ })
4885
+ });
4886
+ const text = await response.text();
4887
+ if (!response.ok) {
4888
+ throw new HttpError(response.status, `MCP ${method} failed with HTTP ${response.status}: ${text}`);
4671
4889
  }
4672
- if (typeof params.cursor !== "string" || !params.cursor) {
4673
- throw new JsonRpcError(-32602, "Pagination cursor must be a non-empty string.");
4890
+ const json = JSON.parse(text);
4891
+ if (json.error) {
4892
+ throw new HttpError(502, json.error.message ?? `MCP ${method} returned an error.`);
4674
4893
  }
4675
- return params.cursor;
4894
+ return json.result;
4895
+ }
4896
+ function toOpenAiMessages(messages) {
4897
+ return [
4898
+ {
4899
+ role: "system",
4900
+ content: [
4901
+ "You are Sidecar dev, a local test harness for an MCP server.",
4902
+ "Use the available MCP tools whenever they are relevant.",
4903
+ "When a tool renders UI, keep your text concise because the harness displays the widget separately.",
4904
+ "Do not claim to be ChatGPT or Claude; the host selector only changes widget styling."
4905
+ ].join(" ")
4906
+ },
4907
+ ...messages.map((message) => ({
4908
+ role: message.role,
4909
+ content: message.content
4910
+ }))
4911
+ ];
4676
4912
  }
4677
- function readUriParam(request, method) {
4678
- const params = request.params;
4679
- const uri = typeof params?.uri === "string" ? params.uri : void 0;
4680
- if (!uri) {
4681
- throw new JsonRpcError(-32602, `${method} requires params.uri.`);
4913
+ function readChatMessages(value) {
4914
+ if (!Array.isArray(value)) {
4915
+ return [];
4682
4916
  }
4683
- return uri;
4917
+ return value.flatMap((entry) => {
4918
+ if (!entry || typeof entry !== "object") {
4919
+ return [];
4920
+ }
4921
+ const role = entry.role;
4922
+ const content = entry.content;
4923
+ if (role !== "user" && role !== "assistant" || typeof content !== "string") {
4924
+ return [];
4925
+ }
4926
+ return [{ role, content }];
4927
+ });
4684
4928
  }
4685
- function readProgressToken(request) {
4686
- const params = request.params;
4687
- if (!params || typeof params !== "object") {
4688
- return void 0;
4929
+ function normalizeToolParameters(schema) {
4930
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
4931
+ return { type: "object", properties: {} };
4689
4932
  }
4690
- const meta = params._meta;
4691
- if (!meta || typeof meta !== "object") {
4692
- return void 0;
4933
+ const record = schema;
4934
+ if (record.type === "object" || record.properties) {
4935
+ return record;
4936
+ }
4937
+ return {
4938
+ type: "object",
4939
+ properties: {}
4940
+ };
4941
+ }
4942
+ function uniqueOpenAiToolName(name, used) {
4943
+ const base = name.replace(/[^A-Za-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^[-_]+/, "").slice(0, 58) || "tool";
4944
+ let candidate = base;
4945
+ let suffix = 2;
4946
+ while (used.has(candidate)) {
4947
+ const tag = `_${suffix}`;
4948
+ candidate = `${base.slice(0, 64 - tag.length)}${tag}`;
4949
+ suffix += 1;
4693
4950
  }
4694
- const progressToken = meta.progressToken;
4695
- if (typeof progressToken === "string") {
4696
- return progressToken;
4951
+ used.add(candidate);
4952
+ return candidate;
4953
+ }
4954
+ function parseToolArguments(value) {
4955
+ if (!value.trim()) {
4956
+ return {};
4697
4957
  }
4698
- if (typeof progressToken === "number" && Number.isInteger(progressToken)) {
4699
- return progressToken;
4958
+ const parsed = JSON.parse(value);
4959
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4960
+ return {};
4700
4961
  }
4701
- return void 0;
4962
+ return parsed;
4702
4963
  }
4703
- function hasProgressToken(request) {
4704
- return readProgressToken(request) !== void 0;
4964
+ function toolResultForModel(result) {
4965
+ const text = (result.content ?? []).map((block) => {
4966
+ if (typeof block === "string") return block;
4967
+ if (block && typeof block === "object" && typeof block.text === "string") {
4968
+ return block.text;
4969
+ }
4970
+ return JSON.stringify(block);
4971
+ }).filter(Boolean).join("\n");
4972
+ const structured = result.structuredContent === void 0 ? "" : `
4973
+
4974
+ structuredContent:
4975
+ ${JSON.stringify(result.structuredContent, null, 2)}`;
4976
+ return `${text || "Tool returned no model-visible content."}${structured}`.slice(0, 8e4);
4705
4977
  }
4706
- function selectPaginationOverride(override, operation) {
4707
- if (!override) {
4708
- return void 0;
4978
+ function createSseSink(response) {
4979
+ return {
4980
+ send(event, data) {
4981
+ response.write(`event: ${event}
4982
+ `);
4983
+ response.write(`data: ${JSON.stringify(data)}
4984
+
4985
+ `);
4986
+ }
4987
+ };
4988
+ }
4989
+ function updateDevState(state, body) {
4990
+ if (body.host === "chatgpt" || body.host === "claude" || body.host === "generic") {
4991
+ state.host = body.host;
4709
4992
  }
4710
- if (typeof override === "function") {
4711
- return override;
4993
+ if (body.theme === "light" || body.theme === "dark") {
4994
+ state.theme = body.theme;
4712
4995
  }
4713
- const key = operationToPaginationKey(operation);
4714
- return override[key] ?? override.default;
4715
- }
4716
- function operationToPaginationKey(operation) {
4717
- switch (operation) {
4718
- case "tools/list":
4719
- return "toolsList";
4720
- case "resources/list":
4721
- return "resourcesList";
4722
- case "resources/templates/list":
4723
- return "resourceTemplatesList";
4724
- case "prompts/list":
4725
- return "promptsList";
4996
+ if (body.device === "desktop" || body.device === "mobile") {
4997
+ state.device = body.device;
4726
4998
  }
4727
4999
  }
4728
- function defaultPagination(items, cursor, pageSize) {
4729
- const offset = cursor ? decodeCursor(cursor) : 0;
4730
- const page = items.slice(offset, offset + pageSize);
4731
- const nextOffset = offset + page.length;
5000
+ function devStatePayload(state, mcpUrl) {
4732
5001
  return {
4733
- items: page,
4734
- nextCursor: nextOffset < items.length ? encodeCursor(nextOffset) : void 0
5002
+ host: state.host,
5003
+ theme: state.theme,
5004
+ device: state.device,
5005
+ target: state.target,
5006
+ model: state.model,
5007
+ mcpUrl
4735
5008
  };
4736
5009
  }
4737
- function encodeCursor(offset) {
4738
- return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
5010
+ function openStateEvents(response, clients, state, mcpUrl) {
5011
+ response.writeHead(200, {
5012
+ "cache-control": "no-cache, no-transform",
5013
+ "connection": "keep-alive",
5014
+ "content-type": "text/event-stream; charset=utf-8"
5015
+ });
5016
+ clients.add(response);
5017
+ createSseSink(response).send("state", devStatePayload(state, mcpUrl));
5018
+ response.on("close", () => clients.delete(response));
4739
5019
  }
4740
- function decodeCursor(cursor) {
4741
- try {
4742
- const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
4743
- const offset = decoded.offset;
4744
- if (!Number.isInteger(offset) || typeof offset !== "number" || offset < 0) {
4745
- throw new Error("invalid offset");
4746
- }
4747
- return offset;
4748
- } catch {
4749
- throw new JsonRpcError(-32602, "Invalid pagination cursor.");
5020
+ function broadcastState(clients, state, mcpUrl) {
5021
+ for (const client of clients) {
5022
+ createSseSink(client).send("state", devStatePayload(state, mcpUrl));
4750
5023
  }
4751
5024
  }
4752
- function authJsonRpcError(result) {
4753
- return new JsonRpcError(
4754
- result.status === 401 ? -32001 : -32003,
4755
- result.body.error_description ?? result.body.error,
4756
- {
4757
- status: result.status,
4758
- headers: headersToRecord(result.headers),
4759
- body: result.body
4760
- }
4761
- );
5025
+ function sendHtml(response, html) {
5026
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
5027
+ response.end(html);
4762
5028
  }
4763
- function validateProtocolVersion(request) {
4764
- const value = request.headers["mcp-protocol-version"];
4765
- const version = Array.isArray(value) ? value[0] : value;
4766
- if (!version) {
4767
- return;
4768
- }
4769
- const supported = /* @__PURE__ */ new Set([SIDECAR_MCP_PROTOCOL_VERSION]);
4770
- if (!supported.has(version)) {
4771
- throw new JsonRpcHttpError(
4772
- 400,
4773
- -32600,
4774
- `Unsupported MCP-Protocol-Version "${version}".`
4775
- );
4776
- }
5029
+ function sendJson(response, body, status = 200) {
5030
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
5031
+ response.end(JSON.stringify(body));
4777
5032
  }
4778
- function validatePostHeaders(request) {
4779
- const contentType = request.headers["content-type"];
4780
- const contentTypeValue = Array.isArray(contentType) ? contentType[0] : contentType;
4781
- if (!contentTypeValue?.toLowerCase().includes("application/json")) {
4782
- throw new JsonRpcHttpError(415, -32600, "POST Content-Type must be application/json.");
4783
- }
4784
- const accept = request.headers.accept;
4785
- const acceptValue = Array.isArray(accept) ? accept.join(",") : accept;
4786
- if (!acceptValue || !acceptValue.toLowerCase().includes("application/json") || !acceptValue.toLowerCase().includes("text/event-stream")) {
4787
- throw new JsonRpcHttpError(
4788
- 406,
4789
- -32600,
4790
- "POST Accept must include application/json and text/event-stream."
4791
- );
5033
+ async function readJson(request) {
5034
+ const chunks = [];
5035
+ let size = 0;
5036
+ for await (const chunk of request) {
5037
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5038
+ size += buffer.byteLength;
5039
+ if (size > 2e6) {
5040
+ throw new HttpError(413, "Request body is too large.");
5041
+ }
5042
+ chunks.push(buffer);
4792
5043
  }
5044
+ const text = Buffer.concat(chunks).toString("utf8");
5045
+ return text ? JSON.parse(text) : {};
4793
5046
  }
4794
- function validateGetHeaders(request) {
4795
- const accept = request.headers.accept;
4796
- const acceptValue = Array.isArray(accept) ? accept.join(",") : accept;
4797
- if (!acceptValue || !acceptValue.toLowerCase().includes("text/event-stream")) {
4798
- throw new JsonRpcHttpError(
4799
- 406,
4800
- -32600,
4801
- "GET Accept must include text/event-stream."
4802
- );
5047
+ function readAuthToken(value) {
5048
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
5049
+ }
5050
+ function readBearerFromCookie(request) {
5051
+ const cookie = request.headers.cookie;
5052
+ if (!cookie) {
5053
+ return void 0;
4803
5054
  }
5055
+ const match = cookie.match(/(?:^|;\s*)sidecar_dev_bearer=([^;]+)/);
5056
+ return match?.[1] ? decodeURIComponent(match[1]) : void 0;
4804
5057
  }
4805
- async function withTimeout(promise, timeoutMs, controller) {
4806
- let abortListener;
4807
- const abortPromise = new Promise((_resolve, reject) => {
4808
- abortListener = () => {
4809
- reject(abortReason(controller.signal.reason));
5058
+ function normalizeHttpError(error) {
5059
+ if (error instanceof HttpError) {
5060
+ return {
5061
+ error: "sidecar_dev_error",
5062
+ status: error.status,
5063
+ message: error.message
4810
5064
  };
4811
- if (controller.signal.aborted) {
4812
- abortListener();
4813
- return;
4814
- }
4815
- controller.signal.addEventListener("abort", abortListener, { once: true });
5065
+ }
5066
+ return {
5067
+ error: "sidecar_dev_error",
5068
+ message: error instanceof Error ? error.message : String(error)
5069
+ };
5070
+ }
5071
+ function listenOnLocalhost(server, port) {
5072
+ return new Promise((resolve, reject) => {
5073
+ const onError = (error) => reject(error);
5074
+ server.once("error", onError);
5075
+ server.listen(port, "127.0.0.1", () => {
5076
+ server.off("error", onError);
5077
+ const address = server.address();
5078
+ if (!address) {
5079
+ reject(new Error("Dev harness did not expose a bound address."));
5080
+ return;
5081
+ }
5082
+ resolve(address.port);
5083
+ });
4816
5084
  });
4817
- if (!timeoutMs || timeoutMs <= 0) {
4818
- try {
4819
- return await Promise.race([promise, abortPromise]);
4820
- } finally {
4821
- if (abortListener) {
4822
- controller.signal.removeEventListener("abort", abortListener);
5085
+ }
5086
+ function closeServer(server) {
5087
+ return new Promise((resolve, reject) => {
5088
+ server.close((error) => {
5089
+ if (error) {
5090
+ reject(error);
5091
+ return;
4823
5092
  }
4824
- }
4825
- }
4826
- let timeout;
4827
- try {
4828
- return await Promise.race([
4829
- promise,
4830
- abortPromise,
4831
- new Promise((_resolve, reject) => {
4832
- timeout = setTimeout(() => {
4833
- const error = new JsonRpcError(-32e3, "Tool execution timed out.");
4834
- controller.abort(error);
4835
- reject(error);
4836
- }, timeoutMs);
4837
- })
4838
- ]);
4839
- } finally {
4840
- if (timeout) {
4841
- clearTimeout(timeout);
4842
- }
4843
- if (abortListener) {
4844
- controller.signal.removeEventListener("abort", abortListener);
4845
- }
4846
- }
5093
+ resolve();
5094
+ });
5095
+ });
4847
5096
  }
4848
- function abortReason(reason) {
4849
- if (reason instanceof Error) {
4850
- return reason;
4851
- }
4852
- return new RequestCancelledError();
5097
+ async function streamdownClientScript() {
5098
+ streamdownClientScriptPromise ??= buildStreamdownClientScript();
5099
+ return await streamdownClientScriptPromise;
4853
5100
  }
4854
- function validateAgainstSchema(schema, value, message, options = {}) {
4855
- if (!schema || value === void 0 && options.optional) {
4856
- return value;
4857
- }
4858
- const failure = schemaFailure(schema, value, "$");
4859
- if (failure) {
4860
- throw new JsonRpcError(-32602, message, { validation: failure });
5101
+ async function buildStreamdownClientScript() {
5102
+ const { build } = await import("esbuild");
5103
+ const result = await build({
5104
+ bundle: true,
5105
+ define: {
5106
+ "process.env.NODE_ENV": JSON.stringify("development")
5107
+ },
5108
+ format: "iife",
5109
+ globalName: "SidecarStreamdownBundle",
5110
+ jsx: "automatic",
5111
+ platform: "browser",
5112
+ stdin: {
5113
+ contents: `
5114
+ import React from "react";
5115
+ import { createRoot } from "react-dom/client";
5116
+ import { Streamdown } from "streamdown";
5117
+
5118
+ const roots = new WeakMap();
5119
+ window.SidecarStreamdown = {
5120
+ render(element, markdown) {
5121
+ let root = roots.get(element);
5122
+ if (!root) {
5123
+ root = createRoot(element);
5124
+ roots.set(element, root);
5125
+ }
5126
+ root.render(React.createElement(Streamdown, { parseIncompleteMarkdown: true }, markdown));
5127
+ }
5128
+ };
5129
+ `,
5130
+ loader: "tsx",
5131
+ resolveDir: path20.dirname(fileURLToPath(import.meta.url))
5132
+ },
5133
+ write: false
5134
+ });
5135
+ const output = result.outputFiles[0]?.text;
5136
+ if (!output) {
5137
+ throw new Error("Failed to build the Sidecar dev markdown renderer.");
4861
5138
  }
4862
- return value;
5139
+ return output;
4863
5140
  }
4864
- function schemaFailure(schema, value, path21) {
4865
- if (schema.anyOf?.length && !schema.anyOf.some((entry) => !schemaFailure(entry, value, path21))) {
4866
- return `${path21} must match one anyOf schema.`;
5141
+ var HttpError = class extends Error {
5142
+ constructor(status, message) {
5143
+ super(message);
5144
+ this.status = status;
5145
+ this.name = "HttpError";
4867
5146
  }
4868
- if (schema.oneOf?.length && schema.oneOf.filter((entry) => !schemaFailure(entry, value, path21)).length !== 1) {
4869
- return `${path21} must match exactly one oneOf schema.`;
5147
+ status;
5148
+ };
5149
+ function devHarnessBrowserScript(initialBearerToken) {
5150
+ return String.raw`
5151
+ const MCP_APPS_PROTOCOL_VERSION = ${JSON.stringify(MCP_APPS_PROTOCOL_VERSION)};
5152
+ const state = {
5153
+ host: document.documentElement.dataset.sidecarHost || "chatgpt",
5154
+ theme: document.documentElement.dataset.sidecarTheme || "light",
5155
+ device: document.documentElement.dataset.sidecarDevice || "desktop",
5156
+ };
5157
+ const messages = [];
5158
+ const frameContexts = new Map();
5159
+ const markdownSources = new WeakMap();
5160
+ let markdownRendererPromise;
5161
+ const messagesEl = document.getElementById("messages");
5162
+ const form = document.getElementById("chatForm");
5163
+ const promptEl = document.getElementById("prompt");
5164
+ const statusEl = document.getElementById("status");
5165
+ const authEl = document.getElementById("authToken");
5166
+ const authTriggerEl = document.getElementById("authTrigger");
5167
+ const authModalEl = document.getElementById("authModal");
5168
+ const authCloseEl = document.getElementById("authClose");
5169
+ const authCancelEl = document.getElementById("authCancel");
5170
+ const authSaveEl = document.getElementById("authSave");
5171
+ const authErrorEl = document.getElementById("authError");
5172
+ const surfaceTitleEl = document.getElementById("surfaceTitle");
5173
+ const surfaceMetaEl = document.getElementById("surfaceMeta");
5174
+ const workspaceTargetEl = document.getElementById("workspaceTarget");
5175
+ const workspaceDeviceEl = document.getElementById("workspaceDevice");
5176
+ const initialBearerToken = ${JSON.stringify(initialBearerToken ?? "")};
5177
+ let bearerToken = initialBearerToken || localStorage.getItem("sidecar.dev.bearer") || "";
5178
+
5179
+ authEl.value = bearerToken;
5180
+ setAuthCookie(bearerToken);
5181
+ updateAuthTrigger();
5182
+ authTriggerEl.addEventListener("click", openAuthModal);
5183
+ authCloseEl.addEventListener("click", closeAuthModal);
5184
+ authCancelEl.addEventListener("click", closeAuthModal);
5185
+ authSaveEl.addEventListener("click", saveBearerToken);
5186
+ authModalEl.addEventListener("click", (event) => {
5187
+ if (event.target === authModalEl) {
5188
+ closeAuthModal();
4870
5189
  }
4871
- if (schema.allOf?.length) {
4872
- for (const entry of schema.allOf) {
4873
- const failure = schemaFailure(entry, value, path21);
4874
- if (failure) return failure;
4875
- }
5190
+ });
5191
+ document.addEventListener("keydown", (event) => {
5192
+ if (event.key === "Escape" && authModalEl.dataset.open === "true") {
5193
+ closeAuthModal();
4876
5194
  }
4877
- if (schema.const !== void 0 && value !== schema.const) {
4878
- return `${path21} must equal ${JSON.stringify(schema.const)}.`;
5195
+ });
5196
+
5197
+ document.querySelectorAll("[data-host]").forEach((button) => {
5198
+ button.addEventListener("click", () => setState({ host: button.dataset.host }));
5199
+ });
5200
+ document.querySelectorAll("[data-theme]").forEach((button) => {
5201
+ button.addEventListener("click", () => setState({ theme: button.dataset.theme }));
5202
+ });
5203
+ document.querySelectorAll("[data-device]").forEach((button) => {
5204
+ button.addEventListener("click", () => setState({ device: button.dataset.device }));
5205
+ });
5206
+
5207
+ const events = new EventSource("/__sidecar/dev/events");
5208
+ events.addEventListener("state", (event) => {
5209
+ applyState(JSON.parse(event.data));
5210
+ });
5211
+
5212
+ form.addEventListener("submit", async (event) => {
5213
+ event.preventDefault();
5214
+ const text = promptEl.value.trim();
5215
+ if (!text) return;
5216
+ promptEl.value = "";
5217
+ clearEmpty();
5218
+ messages.push({ role: "user", content: text });
5219
+ appendMessage("user", text);
5220
+ const assistant = appendMessage("assistant", "");
5221
+ statusEl.textContent = "Thinking...";
5222
+ try {
5223
+ await streamChat(assistant.querySelector(".content"));
5224
+ statusEl.textContent = "";
5225
+ } catch (error) {
5226
+ statusEl.textContent = error instanceof Error ? error.message : String(error);
5227
+ statusEl.classList.add("error");
4879
5228
  }
4880
- if (schema.enum && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) {
4881
- return `${path21} must be one of the declared enum values.`;
5229
+ });
5230
+
5231
+ window.addEventListener("message", async (event) => {
5232
+ const message = event.data;
5233
+ if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
5234
+ return;
4882
5235
  }
4883
- if (schema.type) {
4884
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
4885
- if (!types.some((type) => matchesJsonSchemaType(type, value))) {
4886
- return `${path21} must be ${types.join(" or ")}.`;
5236
+ const frame = [...frameContexts.entries()].find(([source]) => source === event.source);
5237
+ const context = frame?.[1];
5238
+ try {
5239
+ if (message.method === "ui/initialize") {
5240
+ respond(event.source, message.id, {
5241
+ protocolVersion: MCP_APPS_PROTOCOL_VERSION,
5242
+ hostInfo: { name: "Sidecar dev", version: "0.0.0-dev" },
5243
+ hostCapabilities: hostCapabilities(),
5244
+ hostContext: hostContext(),
5245
+ });
5246
+ return;
4887
5247
  }
4888
- }
4889
- if (schema.type === "object" || schema.properties || schema.required) {
4890
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4891
- return `${path21} must be an object.`;
5248
+ if (message.method === "ui/notifications/initialized") {
5249
+ sendFrameContext(event.source);
5250
+ return;
4892
5251
  }
4893
- const record = value;
4894
- for (const required of schema.required ?? []) {
4895
- if (!(required in record)) {
4896
- return `${path21}.${required} is required.`;
4897
- }
5252
+ if (message.method === "tools/call") {
5253
+ const result = await rpc("tools/call", message.params);
5254
+ respond(event.source, message.id, result);
5255
+ return;
4898
5256
  }
4899
- for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
4900
- if (key in record) {
4901
- const failure = schemaFailure(propertySchema, record[key], `${path21}.${key}`);
4902
- if (failure) return failure;
4903
- }
5257
+ if (message.method === "resources/list" || message.method === "resources/read") {
5258
+ const result = await rpc(message.method, message.params);
5259
+ respond(event.source, message.id, result);
5260
+ return;
4904
5261
  }
4905
- const allowed = new Set(Object.keys(schema.properties ?? {}));
4906
- if (schema.additionalProperties === false) {
4907
- const extra = Object.keys(record).find((key) => !allowed.has(key));
4908
- if (extra) {
4909
- return `${path21}.${extra} is not allowed.`;
4910
- }
4911
- } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
4912
- for (const [key, entry] of Object.entries(record)) {
4913
- if (!allowed.has(key)) {
4914
- const failure = schemaFailure(schema.additionalProperties, entry, `${path21}.${key}`);
4915
- if (failure) return failure;
4916
- }
5262
+ if (message.method === "ui/open-link") {
5263
+ const url = message.params?.url;
5264
+ if (typeof url === "string" && /^(https?:|mailto:)/.test(url)) {
5265
+ window.open(url, "_blank", "noopener,noreferrer");
5266
+ respond(event.source, message.id, { isError: false });
5267
+ } else {
5268
+ respond(event.source, message.id, { isError: true });
4917
5269
  }
5270
+ return;
4918
5271
  }
4919
- }
4920
- if (schema.type === "array" || schema.items) {
4921
- if (!Array.isArray(value)) {
4922
- return `${path21} must be an array.`;
4923
- }
4924
- if (schema.minItems !== void 0 && value.length < schema.minItems) {
4925
- return `${path21} must contain at least ${schema.minItems} items.`;
4926
- }
4927
- if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
4928
- return `${path21} must contain at most ${schema.maxItems} items.`;
5272
+ if (message.method === "ui/request-display-mode") {
5273
+ respond(event.source, message.id, { mode: message.params?.mode || "inline" });
5274
+ return;
4929
5275
  }
4930
- if (schema.items) {
4931
- for (const [index, entry] of value.entries()) {
4932
- const failure = schemaFailure(schema.items, entry, `${path21}[${index}]`);
4933
- if (failure) return failure;
4934
- }
5276
+ if (message.method === "ui/message" || message.method === "ui/update-model-context") {
5277
+ respond(event.source, message.id, { isError: false });
5278
+ return;
4935
5279
  }
4936
- }
4937
- if (typeof value === "string") {
4938
- if (schema.minLength !== void 0 && value.length < schema.minLength) {
4939
- return `${path21} is shorter than ${schema.minLength}.`;
5280
+ if (message.method.startsWith("ui/notifications/") || message.method === "notifications/message") {
5281
+ return;
4940
5282
  }
4941
- if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
4942
- return `${path21} is longer than ${schema.maxLength}.`;
5283
+ if (message.id !== undefined) {
5284
+ respondError(event.source, message.id, -32601, "Unsupported dev harness method: " + message.method);
4943
5285
  }
4944
- if (schema.pattern !== void 0 && !new RegExp(schema.pattern).test(value)) {
4945
- return `${path21} must match pattern ${schema.pattern}.`;
5286
+ } catch (error) {
5287
+ if (message.id !== undefined) {
5288
+ respondError(event.source, message.id, -32000, error instanceof Error ? error.message : String(error));
4946
5289
  }
4947
5290
  }
4948
- if (typeof value === "number") {
4949
- if (schema.minimum !== void 0 && value < schema.minimum) {
4950
- return `${path21} is less than ${schema.minimum}.`;
4951
- }
4952
- if (schema.maximum !== void 0 && value > schema.maximum) {
4953
- return `${path21} is greater than ${schema.maximum}.`;
5291
+ });
5292
+
5293
+ async function streamChat(contentEl) {
5294
+ const response = await fetch("/__sidecar/dev/chat", {
5295
+ method: "POST",
5296
+ headers: { "content-type": "application/json" },
5297
+ body: JSON.stringify({
5298
+ messages,
5299
+ authToken: currentAuthToken(),
5300
+ }),
5301
+ });
5302
+ if (!response.ok || !response.body) {
5303
+ throw new Error("Chat request failed.");
5304
+ }
5305
+ const reader = response.body.getReader();
5306
+ const decoder = new TextDecoder();
5307
+ let buffer = "";
5308
+ let assistantText = "";
5309
+ while (true) {
5310
+ const { done, value } = await reader.read();
5311
+ if (done) break;
5312
+ buffer += decoder.decode(value, { stream: true });
5313
+ let boundary = buffer.indexOf("\n\n");
5314
+ while (boundary !== -1) {
5315
+ const raw = buffer.slice(0, boundary);
5316
+ buffer = buffer.slice(boundary + 2);
5317
+ const parsed = parseEvent(raw);
5318
+ if (parsed) {
5319
+ if (parsed.event === "delta") {
5320
+ assistantText += parsed.data.text || "";
5321
+ renderMarkdown(contentEl, assistantText);
5322
+ } else if (parsed.event === "tool_start") {
5323
+ appendToolStart(parsed.data);
5324
+ } else if (parsed.event === "tool_result") {
5325
+ appendToolResult(parsed.data);
5326
+ } else if (parsed.event === "error") {
5327
+ throw new Error(parsed.data.message || "Sidecar dev chat failed.");
5328
+ }
5329
+ }
5330
+ boundary = buffer.indexOf("\n\n");
4954
5331
  }
4955
5332
  }
4956
- return void 0;
4957
- }
4958
- function matchesJsonSchemaType(type, value) {
4959
- switch (type) {
4960
- case "object":
4961
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
4962
- case "array":
4963
- return Array.isArray(value);
4964
- case "string":
4965
- return typeof value === "string";
4966
- case "number":
4967
- return typeof value === "number" && Number.isFinite(value);
4968
- case "integer":
4969
- return Number.isInteger(value);
4970
- case "boolean":
4971
- return typeof value === "boolean";
4972
- case "null":
4973
- return value === null;
4974
- default:
4975
- return true;
5333
+ if (assistantText.trim()) {
5334
+ messages.push({ role: "assistant", content: assistantText });
4976
5335
  }
4977
5336
  }
4978
- function headersToRecord(headers) {
4979
- const record = {};
4980
- headers.forEach((value, key) => {
4981
- record[key] = value;
4982
- });
4983
- return record;
4984
- }
4985
- function stripUndefined4(value) {
4986
- return Object.fromEntries(
4987
- Object.entries(value).filter(([, entry]) => entry !== void 0)
4988
- );
5337
+
5338
+ function parseEvent(raw) {
5339
+ let event = "message";
5340
+ let data = "";
5341
+ for (const line of raw.split(/\r?\n/)) {
5342
+ if (line.startsWith("event:")) event = line.slice(6).trim();
5343
+ if (line.startsWith("data:")) data += line.slice(5).trim();
5344
+ }
5345
+ if (!data) return null;
5346
+ return { event, data: JSON.parse(data) };
4989
5347
  }
4990
- function httpAuthError(value) {
4991
- if (!value || typeof value !== "object" || !("error" in value)) {
4992
- return void 0;
5348
+
5349
+ function appendMessage(role, content) {
5350
+ clearEmpty();
5351
+ const article = document.createElement("article");
5352
+ article.className = "message";
5353
+ article.dataset.role = role;
5354
+ article.innerHTML = '<div class="role"></div><div class="content"></div>';
5355
+ article.querySelector(".role").textContent = role;
5356
+ const contentEl = article.querySelector(".content");
5357
+ if (role === "assistant") {
5358
+ contentEl.classList.add("markdown");
5359
+ renderMarkdown(contentEl, content);
5360
+ } else {
5361
+ contentEl.textContent = content;
4993
5362
  }
4994
- const error = value.error;
4995
- const data = error?.data;
4996
- if (!data || typeof data !== "object") {
4997
- return void 0;
5363
+ messagesEl.append(article);
5364
+ article.scrollIntoView({ block: "end" });
5365
+ return article;
5366
+ }
5367
+
5368
+ function renderMarkdown(element, markdown) {
5369
+ markdownSources.set(element, markdown);
5370
+ if (window.SidecarStreamdown?.render) {
5371
+ window.SidecarStreamdown.render(element, markdown);
5372
+ return;
4998
5373
  }
4999
- const status = data.status;
5000
- const headers = data.headers;
5001
- if (status !== 401 && status !== 403 || !headers || typeof headers !== "object") {
5002
- return void 0;
5374
+ element.textContent = markdown;
5375
+ if (!markdownRendererPromise) {
5376
+ markdownRendererPromise = import("/__sidecar/dev/streamdown-client.js")
5377
+ .then(() => {
5378
+ document.querySelectorAll(".content.markdown").forEach((target) => {
5379
+ window.SidecarStreamdown?.render(target, markdownSources.get(target) || "");
5380
+ });
5381
+ })
5382
+ .catch((error) => {
5383
+ console.warn("Sidecar dev markdown renderer failed to load", error);
5384
+ });
5003
5385
  }
5004
- return {
5005
- status,
5006
- headers
5007
- };
5008
5386
  }
5009
- var consoleLogger = {
5010
- debug(message, data) {
5011
- console.debug(message, data ?? "");
5012
- },
5013
- info(message, data) {
5014
- console.info(message, data ?? "");
5015
- },
5016
- warn(message, data) {
5017
- console.warn(message, data ?? "");
5018
- },
5019
- error(message, data) {
5020
- console.error(message, data ?? "");
5387
+
5388
+ function appendToolStart(tool) {
5389
+ const article = document.createElement("article");
5390
+ article.className = "tool-card";
5391
+ article.dataset.toolCallId = tool.id;
5392
+ article.innerHTML = '<div class="tool-head"><div class="tool-title"></div><div class="status">Running</div></div><div class="tool-body"></div>';
5393
+ article.querySelector(".tool-title").textContent = tool.title || tool.name;
5394
+ messagesEl.append(article);
5395
+ }
5396
+
5397
+ function appendToolResult(event) {
5398
+ const article = document.querySelector('[data-tool-call-id="' + CSS.escape(event.id) + '"]') || document.createElement("article");
5399
+ if (!article.parentElement) {
5400
+ article.className = "tool-card";
5401
+ article.dataset.toolCallId = event.id;
5402
+ article.innerHTML = '<div class="tool-head"><div class="tool-title"></div><div class="status"></div></div><div class="tool-body"></div>';
5403
+ messagesEl.append(article);
5404
+ }
5405
+ article.querySelector(".tool-title").textContent = event.tool.title || event.tool.name;
5406
+ article.querySelector(".status").textContent = event.result?.isError ? "Error" : "Done";
5407
+ const body = article.querySelector(".tool-body");
5408
+ body.innerHTML = "";
5409
+ if (event.tool.resourceUri) {
5410
+ const iframe = document.createElement("iframe");
5411
+ iframe.src = "/__sidecar/dev/resource?uri=" + encodeURIComponent(event.tool.resourceUri);
5412
+ iframe.title = event.tool.title || event.tool.name;
5413
+ frameContexts.set(iframe.contentWindow, {
5414
+ result: event.result,
5415
+ tool: event.tool,
5416
+ arguments: event.arguments || {},
5417
+ });
5418
+ iframe.addEventListener("load", () => {
5419
+ frameContexts.set(iframe.contentWindow, {
5420
+ result: event.result,
5421
+ tool: event.tool,
5422
+ arguments: event.arguments || {},
5423
+ });
5424
+ window.setTimeout(() => sendFrameContext(iframe.contentWindow), 120);
5425
+ });
5426
+ body.append(iframe);
5427
+ } else {
5428
+ const pre = document.createElement("pre");
5429
+ pre.textContent = toolText(event.result);
5430
+ body.append(pre);
5021
5431
  }
5022
- };
5023
- async function readJson(request, maxBodyBytes) {
5024
- const chunks = [];
5025
- let size = 0;
5026
- for await (const chunk of request) {
5027
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5028
- size += buffer.byteLength;
5029
- if (size > maxBodyBytes) {
5030
- throw new JsonRpcHttpError(413, -32600, "Request body is too large.");
5031
- }
5032
- chunks.push(buffer);
5432
+ article.scrollIntoView({ block: "end" });
5433
+ }
5434
+
5435
+ function toolText(result) {
5436
+ const blocks = result?.content || [];
5437
+ const text = blocks.map((block) => typeof block === "string" ? block : block?.text || JSON.stringify(block)).join("\n");
5438
+ return text || JSON.stringify(result?.structuredContent || result || {}, null, 2);
5439
+ }
5440
+
5441
+ async function rpc(method, params) {
5442
+ const response = await fetch("/__sidecar/dev/rpc", {
5443
+ method: "POST",
5444
+ headers: { "content-type": "application/json" },
5445
+ body: JSON.stringify({ method, params, authToken: currentAuthToken() }),
5446
+ });
5447
+ const json = await response.json();
5448
+ if (!response.ok) {
5449
+ throw new Error(json.message || json.error || "MCP request failed.");
5033
5450
  }
5034
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
5451
+ return json.result;
5035
5452
  }
5036
- function toFetchRequest(request, baseUrl) {
5037
- const headers = new Headers();
5038
- for (const [key, value] of Object.entries(request.headers)) {
5039
- if (Array.isArray(value)) {
5040
- for (const entry of value) {
5041
- headers.append(key, entry);
5042
- }
5043
- } else if (value !== void 0) {
5044
- headers.set(key, value);
5453
+
5454
+ function openAuthModal() {
5455
+ authEl.value = bearerToken;
5456
+ authErrorEl.textContent = "";
5457
+ authErrorEl.dataset.visible = "false";
5458
+ authModalEl.dataset.open = "true";
5459
+ authModalEl.setAttribute("aria-hidden", "false");
5460
+ window.setTimeout(() => authEl.focus(), 40);
5461
+ }
5462
+
5463
+ function closeAuthModal() {
5464
+ authModalEl.dataset.open = "false";
5465
+ authModalEl.setAttribute("aria-hidden", "true");
5466
+ window.setTimeout(() => authTriggerEl.focus(), 40);
5467
+ }
5468
+
5469
+ async function saveBearerToken() {
5470
+ const nextToken = authEl.value.trim();
5471
+ authErrorEl.textContent = "";
5472
+ authErrorEl.dataset.visible = "false";
5473
+ authSaveEl.disabled = true;
5474
+ authSaveEl.textContent = nextToken ? "Testing..." : "Saving...";
5475
+ try {
5476
+ if (nextToken) {
5477
+ await testBearerToken(nextToken);
5478
+ }
5479
+ bearerToken = nextToken;
5480
+ if (bearerToken) {
5481
+ localStorage.setItem("sidecar.dev.bearer", bearerToken);
5482
+ } else {
5483
+ localStorage.removeItem("sidecar.dev.bearer");
5045
5484
  }
5485
+ setAuthCookie(bearerToken);
5486
+ updateAuthTrigger();
5487
+ closeAuthModal();
5488
+ } catch (error) {
5489
+ authErrorEl.textContent = error instanceof Error ? error.message : String(error);
5490
+ authErrorEl.dataset.visible = "true";
5491
+ } finally {
5492
+ authSaveEl.disabled = false;
5493
+ authSaveEl.textContent = "Save";
5046
5494
  }
5047
- const url = baseUrl ? new URL(request.url ?? "/", baseUrl) : new URL(
5048
- request.url ?? "/",
5049
- `${headers.get("x-forwarded-proto") ?? "http"}://${headers.get("host") ?? "127.0.0.1"}`
5050
- );
5051
- return new Request(url, {
5052
- headers,
5053
- method: request.method
5495
+ }
5496
+
5497
+ async function testBearerToken(token) {
5498
+ const response = await fetch("/__sidecar/dev/rpc", {
5499
+ method: "POST",
5500
+ headers: { "content-type": "application/json" },
5501
+ body: JSON.stringify({ method: "tools/list", params: {}, authToken: token }),
5054
5502
  });
5503
+ const json = await response.json().catch(() => ({}));
5504
+ if (!response.ok) {
5505
+ throw new Error(json.message || json.error || "Bearer token test failed.");
5506
+ }
5055
5507
  }
5056
- function assertJsonRpcRequest(value) {
5057
- if (!value || typeof value !== "object") {
5058
- throw new JsonRpcError(-32600, "Request must be a JSON object.");
5508
+
5509
+ function currentAuthToken() {
5510
+ return bearerToken.trim() || undefined;
5511
+ }
5512
+
5513
+ function updateAuthTrigger() {
5514
+ authTriggerEl.dataset.tokenSet = String(Boolean(currentAuthToken()));
5515
+ authTriggerEl.title = currentAuthToken() ? "Bearer token is set" : "Set bearer token";
5516
+ }
5517
+
5518
+ async function setState(next) {
5519
+ await fetch("/__sidecar/dev/state", {
5520
+ method: "POST",
5521
+ headers: { "content-type": "application/json" },
5522
+ body: JSON.stringify(next),
5523
+ });
5524
+ }
5525
+
5526
+ function applyState(next) {
5527
+ state.host = next.host || state.host;
5528
+ state.theme = next.theme || state.theme;
5529
+ state.device = next.device || state.device;
5530
+ document.documentElement.dataset.sidecarHost = state.host;
5531
+ document.documentElement.dataset.sidecarTheme = state.theme;
5532
+ document.documentElement.dataset.sidecarDevice = state.device;
5533
+ document.querySelectorAll("[data-host]").forEach((button) => {
5534
+ button.setAttribute("aria-pressed", String(button.dataset.host === state.host));
5535
+ });
5536
+ document.querySelectorAll("[data-theme]").forEach((button) => {
5537
+ button.setAttribute("aria-pressed", String(button.dataset.theme === state.theme));
5538
+ });
5539
+ document.querySelectorAll("[data-device]").forEach((button) => {
5540
+ button.setAttribute("aria-pressed", String(button.dataset.device === state.device));
5541
+ });
5542
+ syncSegmentedControl("[data-host]", state.host, "host");
5543
+ syncSegmentedControl("[data-theme]", state.theme, "theme");
5544
+ syncSegmentedControl("[data-device]", state.device, "device");
5545
+ surfaceTitleEl.textContent = (state.host === "generic" ? "MCP" : state.host === "chatgpt" ? "ChatGPT" : "Claude") + " preview";
5546
+ surfaceMetaEl.textContent = state.device === "mobile" ? "Mobile" : "Desktop";
5547
+ if (workspaceTargetEl) {
5548
+ workspaceTargetEl.textContent = state.host === "generic" ? "MCP" : state.host === "chatgpt" ? "ChatGPT" : "Claude";
5059
5549
  }
5060
- const record = value;
5061
- if (record.jsonrpc !== JSONRPC_VERSION2 || typeof record.method !== "string") {
5062
- throw new JsonRpcError(-32600, "Invalid JSON-RPC request.");
5550
+ if (workspaceDeviceEl) {
5551
+ workspaceDeviceEl.textContent = state.device === "mobile" ? "Mobile" : "Desktop";
5063
5552
  }
5064
- if ("id" in record && record.id !== void 0 && record.id !== null && typeof record.id !== "string" && typeof record.id !== "number") {
5065
- throw new JsonRpcError(-32600, "JSON-RPC id must be a string, number, or null.");
5553
+ for (const source of frameContexts.keys()) {
5554
+ notify(source, "ui/notifications/host-context-changed", hostContext());
5066
5555
  }
5067
- return {
5068
- jsonrpc: JSONRPC_VERSION2,
5069
- id: typeof record.id === "string" || typeof record.id === "number" || record.id === null ? record.id : void 0,
5070
- method: record.method,
5071
- params: record.params
5072
- };
5073
5556
  }
5074
- function isJsonRpcResponseMessage(value) {
5075
- if (!value || typeof value !== "object") {
5076
- return false;
5077
- }
5078
- const record = value;
5079
- if (record.jsonrpc !== JSONRPC_VERSION2 || !("id" in record) || !("result" in record || "error" in record)) {
5080
- return false;
5557
+
5558
+ function syncSegmentedControl(selector, value, datasetKey) {
5559
+ const buttons = [...document.querySelectorAll(selector)];
5560
+ const segmented = buttons[0]?.closest(".segmented");
5561
+ if (!segmented) {
5562
+ return;
5081
5563
  }
5082
- return record.id === null || typeof record.id === "string" || typeof record.id === "number";
5564
+ const activeIndex = Math.max(0, buttons.findIndex((button) => button.dataset[datasetKey] === value));
5565
+ segmented.style.setProperty("--segments", String(buttons.length));
5566
+ segmented.style.setProperty("--active-index", String(activeIndex));
5083
5567
  }
5084
- function normalizeError(error) {
5085
- if (error instanceof JsonRpcError) {
5086
- return {
5087
- code: error.code,
5088
- message: error.message,
5089
- data: error.data
5090
- };
5091
- }
5092
- if (error instanceof SidecarRuntimeError && error.code === "invalid_pagination_cursor") {
5093
- return {
5094
- code: -32602,
5095
- message: error.message
5096
- };
5097
- }
5098
- if (error instanceof Error) {
5099
- return {
5100
- code: -32e3,
5101
- message: "Internal server error."
5102
- };
5568
+
5569
+ function sendFrameContext(source) {
5570
+ const context = frameContexts.get(source);
5571
+ if (!context) {
5572
+ return;
5103
5573
  }
5574
+ notify(source, "ui/notifications/host-context-changed", hostContext());
5575
+ notify(source, "ui/notifications/tool-input", { arguments: context.arguments || {} });
5576
+ notify(source, "ui/notifications/tool-result", context.result);
5577
+ }
5578
+
5579
+ function hostContext() {
5104
5580
  return {
5105
- code: -32e3,
5106
- message: "Unknown server error.",
5107
- data: error
5581
+ theme: state.theme,
5582
+ device: state.device,
5583
+ userAgent: "Sidecar Dev " + state.host,
5584
+ displayMode: "inline",
5585
+ availableDisplayModes: ["inline", "fullscreen", "pip"],
5586
+ platform: "web",
5587
+ styles: { variables: hostVariables() },
5108
5588
  };
5109
5589
  }
5110
- function normalizeHttpError(error) {
5111
- if (error instanceof JsonRpcError) {
5112
- return {
5113
- code: error.code,
5114
- message: error.message,
5115
- data: error.data
5116
- };
5590
+
5591
+ function hostVariables() {
5592
+ if (state.host === "claude") {
5593
+ return state.theme === "dark"
5594
+ ? { "--font-sans": '"Anthropic Sans", ui-sans-serif, system-ui, sans-serif', "--color-background-primary": "#30302e", "--color-text-primary": "#faf9f5" }
5595
+ : { "--font-sans": '"Anthropic Sans", ui-sans-serif, system-ui, sans-serif', "--color-background-primary": "#ffffff", "--color-text-primary": "#141413" };
5117
5596
  }
5597
+ return {};
5598
+ }
5599
+
5600
+ function hostCapabilities() {
5118
5601
  return {
5119
- code: -32700,
5120
- message: "Invalid JSON request body."
5602
+ serverTools: {},
5603
+ serverResources: {},
5604
+ openLinks: {},
5605
+ logging: {},
5606
+ message: { text: {}, structuredContent: {} },
5607
+ updateModelContext: { structuredContent: {} },
5608
+ downloadFile: {},
5121
5609
  };
5122
5610
  }
5123
5611
 
5612
+ function respond(source, id, result) {
5613
+ if (id === undefined) return;
5614
+ source?.postMessage({ jsonrpc: "2.0", id, result }, "*");
5615
+ }
5616
+
5617
+ function respondError(source, id, code, message) {
5618
+ source?.postMessage({ jsonrpc: "2.0", id, error: { code, message } }, "*");
5619
+ }
5620
+
5621
+ function notify(source, method, params) {
5622
+ source?.postMessage({ jsonrpc: "2.0", method, params }, "*");
5623
+ }
5624
+
5625
+ function setAuthCookie(token) {
5626
+ document.cookie = "sidecar_dev_bearer=" + encodeURIComponent(token || "") + "; path=/; SameSite=Lax";
5627
+ }
5628
+
5629
+ function clearEmpty() {
5630
+ const empty = messagesEl.querySelector(".empty");
5631
+ empty?.remove();
5632
+ }
5633
+
5634
+ applyState(state);
5635
+ `;
5636
+ }
5637
+
5124
5638
  // packages/cli/src/tunnel.ts
5125
5639
  import { spawn, spawnSync } from "child_process";
5126
5640
  import { createInterface } from "readline/promises";
@@ -5128,11 +5642,11 @@ import { stdin, stdout } from "process";
5128
5642
  async function startTunnel(options) {
5129
5643
  const provider = await resolveProvider(options.provider);
5130
5644
  const localUrl = `http://127.0.0.1:${options.port}`;
5131
- const path21 = options.path ?? "/mcp";
5645
+ const path22 = options.path ?? "/mcp";
5132
5646
  if (provider === "cloudflared") {
5133
- return startCloudflared(localUrl, path21, options.timeoutMs);
5647
+ return startCloudflared(localUrl, path22, options.timeoutMs);
5134
5648
  }
5135
- return startWrangler(localUrl, path21, options.timeoutMs);
5649
+ return startWrangler(localUrl, path22, options.timeoutMs);
5136
5650
  }
5137
5651
  async function validateTunnelEndpoint(options) {
5138
5652
  const timeoutMs = options.timeoutMs ?? 1e4;
@@ -5256,7 +5770,7 @@ function assertNpxAvailable() {
5256
5770
  throw new Error(tunnelInstallMessage("wrangler"));
5257
5771
  }
5258
5772
  }
5259
- async function startCloudflared(localUrl, path21, timeoutMs = 2e4) {
5773
+ async function startCloudflared(localUrl, path22, timeoutMs = 2e4) {
5260
5774
  const child = spawn("cloudflared", ["tunnel", "--url", localUrl, "--no-autoupdate"], {
5261
5775
  stdio: ["ignore", "pipe", "pipe"]
5262
5776
  });
@@ -5264,13 +5778,13 @@ async function startCloudflared(localUrl, path21, timeoutMs = 2e4) {
5264
5778
  return {
5265
5779
  provider: "cloudflared",
5266
5780
  publicUrl,
5267
- mcpUrl: appendPath(publicUrl, path21),
5781
+ mcpUrl: appendPath(publicUrl, path22),
5268
5782
  close() {
5269
5783
  child.kill("SIGTERM");
5270
5784
  }
5271
5785
  };
5272
5786
  }
5273
- async function startWrangler(localUrl, path21, timeoutMs = 2e4) {
5787
+ async function startWrangler(localUrl, path22, timeoutMs = 2e4) {
5274
5788
  const child = spawn("npx", ["--yes", "wrangler", "tunnel", "quick-start", localUrl], {
5275
5789
  stdio: ["ignore", "pipe", "pipe"]
5276
5790
  });
@@ -5278,7 +5792,7 @@ async function startWrangler(localUrl, path21, timeoutMs = 2e4) {
5278
5792
  return {
5279
5793
  provider: "wrangler",
5280
5794
  publicUrl,
5281
- mcpUrl: appendPath(publicUrl, path21),
5795
+ mcpUrl: appendPath(publicUrl, path22),
5282
5796
  close() {
5283
5797
  child.kill("SIGTERM");
5284
5798
  }
@@ -5539,8 +6053,8 @@ function runInherited(command, args) {
5539
6053
  });
5540
6054
  });
5541
6055
  }
5542
- function appendPath(origin, path21) {
5543
- return `${origin.replace(/\/+$/, "")}/${path21.replace(/^\/+/, "")}`;
6056
+ function appendPath(origin, path22) {
6057
+ return `${origin.replace(/\/+$/, "")}/${path22.replace(/^\/+/, "")}`;
5544
6058
  }
5545
6059
  function isRecord3(value) {
5546
6060
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -5568,10 +6082,17 @@ async function main(argv) {
5568
6082
  );
5569
6083
  console.log(`Host runtime: ${manifest.host}`);
5570
6084
  if (manifest.host === "vercel") {
5571
- console.log(`Vercel MCP function: ${path20.join(resolvedOutDir, VERCEL_ENTRYPOINT)}`);
6085
+ console.log(`Vercel MCP function: ${path21.join(resolvedOutDir, VERCEL_ENTRYPOINT)}`);
5572
6086
  } else {
5573
- console.log(`Hostable MCP server: ${path20.join(resolvedOutDir, SERVER_ENTRYPOINT)}`);
6087
+ console.log(`Hostable MCP server: ${path21.join(resolvedOutDir, SERVER_ENTRYPOINT)}`);
5574
6088
  }
6089
+ console.log(renderBuildUrlSummary({
6090
+ host: manifest.host,
6091
+ mcpPath: process.env.SIDECAR_MCP_PATH,
6092
+ publicMcpUrl: process.env.SIDECAR_MCP_URL,
6093
+ publicUrl: process.env.SIDECAR_PUBLIC_URL,
6094
+ port: process.env.SIDECAR_PORT ?? process.env.PORT
6095
+ }));
5575
6096
  if (plugins ?? manifest.config.build.plugins) {
5576
6097
  console.log("Built claude-plugin package.");
5577
6098
  }
@@ -5599,70 +6120,69 @@ async function main(argv) {
5599
6120
  return;
5600
6121
  }
5601
6122
  case "dev": {
5602
- const port = Number(readOption(argv, "--port") ?? "3001");
6123
+ await loadProjectEnv(rootDir);
6124
+ const port = Number(readOption(argv, "--mcp-port") ?? readOption(argv, "--port") ?? "3101");
6125
+ const harnessPort = Number(readOption(argv, "--harness-port") ?? "3000");
5603
6126
  const target = readTarget(argv);
6127
+ const devHost = readDevHost(argv);
6128
+ const devTheme = readDevTheme(argv);
6129
+ const devDevice = readDevDevice(argv);
6130
+ const model = readOption(argv, "--model") ?? process.env.SIDECAR_DEV_MODEL ?? "gpt-4.1-mini";
5604
6131
  const tunnelProvider = readTunnelProvider(argv);
5605
6132
  const outDir = `.sidecar/dev/${target}`;
5606
- const loadedAuth = await loadRuntimeAuth(rootDir);
5607
- const runtimeConfig = await loadRuntimeConfig(rootDir);
5608
- const proxy = await loadRuntimeProxy(rootDir);
5609
- const runtimeToolEntries = await analyzeProjectTools(rootDir, { target });
5610
- const runtimeTools = await loadRuntimeTools(rootDir, runtimeToolEntries);
5611
- const manifest = await buildProject({ rootDir, outDir, target });
6133
+ const localMcpUrl = `http://127.0.0.1:${port}/mcp`;
6134
+ process.env.SIDECAR_MCP_URL = localMcpUrl;
6135
+ const manifest = await buildProject({ rootDir, outDir, target, host: "node" });
5612
6136
  printDiagnostics(manifest.diagnostics ?? []);
5613
- const tools = attachBuiltToolDescriptors(runtimeTools, manifest);
5614
6137
  let tunnel;
5615
6138
  if (tunnelProvider) {
5616
6139
  tunnel = await startTunnel({ provider: tunnelProvider, port, path: "/mcp" });
5617
6140
  process.env.SIDECAR_MCP_URL = tunnel.mcpUrl;
5618
6141
  }
5619
- const auth = loadedAuth && tunnel ? loadedAuth.withResource(tunnel.mcpUrl) : loadedAuth;
5620
- const resources = await loadResources(rootDir, outDir, manifest);
5621
- const prompts = await loadRuntimePrompts(rootDir, manifest);
5622
- const server = createSidecarHttpServer({
5623
- name: "sidecar-dev",
5624
- version: "0.0.0-dev",
5625
- path: "/mcp",
5626
- auth,
5627
- proxy,
5628
- tools,
5629
- resources,
5630
- prompts,
5631
- capabilities: {
5632
- tools: runtimeConfig?.tools ?? manifest.config.tools,
5633
- resources: runtimeConfig?.resources ?? manifest.config.resources,
5634
- prompts: runtimeConfig?.prompts ?? manifest.config.prompts
5635
- },
5636
- pagination: runtimeConfig?.pagination ?? {
5637
- pageSize: manifest.config.pagination.pageSize
5638
- }
5639
- });
5640
- let listening = false;
6142
+ const runtimeMcpUrl = tunnel?.mcpUrl ?? localMcpUrl;
6143
+ let mcpProcess;
6144
+ let harness;
5641
6145
  try {
5642
- await listenOnLocalhost(server, port);
5643
- listening = true;
6146
+ mcpProcess = await startBuiltMcpServer({
6147
+ rootDir,
6148
+ outDir,
6149
+ port,
6150
+ mcpUrl: runtimeMcpUrl
6151
+ });
5644
6152
  if (tunnel) {
5645
6153
  await validateTunnelEndpoint({
5646
6154
  mcpUrl: tunnel.mcpUrl,
5647
- auth: Boolean(auth)
6155
+ auth: existsSync8(path21.join(rootDir, "auth.ts"))
5648
6156
  });
5649
6157
  }
5650
- console.log(`MCP running on Streamable HTTP (${target}) at http://127.0.0.1:${port}/mcp`);
5651
- 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"}.`);
5652
- if (tunnel) {
5653
- console.log(`HTTPS tunnel (${tunnel.provider}) validated: ${tunnel.mcpUrl}`);
5654
- console.log("Use this HTTPS MCP URL in ChatGPT, Claude, or a Claude plugin install.");
5655
- }
6158
+ console.log(`MCP running on Streamable HTTP (${target}).`);
6159
+ console.log(`Loaded ${manifest.tools.length} 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"}.`);
6160
+ harness = await startDevHarness({
6161
+ rootDir,
6162
+ mcpUrl: localMcpUrl,
6163
+ host: devHost,
6164
+ theme: devTheme,
6165
+ device: devDevice,
6166
+ target,
6167
+ port: harnessPort,
6168
+ model,
6169
+ initialBearerToken: process.env.MCP_BEARER
6170
+ });
6171
+ console.log(renderDevUrlSummary({
6172
+ localMcpUrl,
6173
+ runtimeMcpUrl,
6174
+ tunnelProvider: tunnel?.provider,
6175
+ harnessUrl: harness.url
6176
+ }));
5656
6177
  } catch (error) {
5657
6178
  tunnel?.close();
5658
- if (listening) {
5659
- await closeServer(server);
5660
- }
6179
+ await harness?.close();
6180
+ await stopBuiltMcpServer(mcpProcess);
5661
6181
  throw error;
5662
6182
  }
5663
6183
  const shutdown = () => {
5664
6184
  tunnel?.close();
5665
- server.close(() => exit(0));
6185
+ void harness?.close().then(() => stopBuiltMcpServer(mcpProcess)).finally(() => exit(0));
5666
6186
  };
5667
6187
  process.once("SIGINT", shutdown);
5668
6188
  process.once("SIGTERM", shutdown);
@@ -5683,9 +6203,13 @@ async function main(argv) {
5683
6203
  return;
5684
6204
  }
5685
6205
  case "preview": {
5686
- if (argv[3] !== "components") {
5687
- throw new Error("Only `sidecar preview components` is supported right now.");
5688
- }
6206
+ await previewProject({
6207
+ rootDir,
6208
+ targets: readPreviewTargets(argv)
6209
+ });
6210
+ return;
6211
+ }
6212
+ case "component-preview": {
5689
6213
  await previewComponents({
5690
6214
  rootDir,
5691
6215
  host: readOption(argv, "--host") ?? "chatgpt",
@@ -5704,9 +6228,10 @@ async function main(argv) {
5704
6228
  Usage:
5705
6229
  sidecar build [--cwd <dir>] [--target mcp|chatgpt|claude] [--host node|vercel] [--out <dir>] [--plugins|--no-plugins] [--strict]
5706
6230
  sidecar check [--cwd <dir>] [--target mcp|chatgpt|claude] [--strict]
5707
- sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <port>] [--tunnel [cloudflared|wrangler]]
6231
+ sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <mcp-port>] [--mcp-port <port>] [--harness-port <port>] [--host chatgpt|claude|generic] [--theme light|dark] [--device desktop|mobile] [--model <model>] [--tunnel [cloudflared|wrangler]]
5708
6232
  sidecar inspect [--cwd <dir>] [--target mcp|chatgpt|claude]
5709
- sidecar preview components [--cwd <dir>] [--host chatgpt|claude|generic] [--compare native,openai] [--components representative|all] [--theme light|dark|both] [--port <port>] [--no-approve]
6233
+ sidecar preview [--cwd <dir>] [--target mcp|chatgpt|claude]
6234
+ sidecar component-preview [--cwd <dir>] [--host chatgpt|claude|generic] [--compare native,openai] [--components representative|all] [--theme light|dark|both] [--port <port>] [--no-approve]
5710
6235
  `);
5711
6236
  }
5712
6237
  }
@@ -5740,12 +6265,59 @@ function readOptionalHost(argv) {
5740
6265
  }
5741
6266
  throw new Error(`Unsupported Sidecar host "${host}". Expected node or vercel.`);
5742
6267
  }
6268
+ function readDevHost(argv) {
6269
+ const host = readOption(argv, "--host") ?? "chatgpt";
6270
+ if (host === "chatgpt" || host === "claude" || host === "generic") {
6271
+ return host;
6272
+ }
6273
+ throw new Error(`Unsupported dev host "${host}". Expected chatgpt, claude, or generic.`);
6274
+ }
6275
+ function readDevTheme(argv) {
6276
+ const theme = readOption(argv, "--theme") ?? "light";
6277
+ if (theme === "light" || theme === "dark") {
6278
+ return theme;
6279
+ }
6280
+ throw new Error(`Unsupported dev theme "${theme}". Expected light or dark.`);
6281
+ }
6282
+ function readDevDevice(argv) {
6283
+ const device = readOption(argv, "--device") ?? "desktop";
6284
+ if (device === "desktop" || device === "mobile") {
6285
+ return device;
6286
+ }
6287
+ throw new Error(`Unsupported dev device "${device}". Expected desktop or mobile.`);
6288
+ }
5743
6289
  function detectHostFromEnvironment() {
5744
6290
  if (process.env.VERCEL === "1") {
5745
6291
  return "vercel";
5746
6292
  }
5747
6293
  return void 0;
5748
6294
  }
6295
+ async function loadProjectEnv(rootDir) {
6296
+ for (const filename of [".env", ".env.local", ".envb"]) {
6297
+ const text = await readFile8(path21.join(rootDir, filename), "utf8").catch(() => void 0);
6298
+ if (!text) {
6299
+ continue;
6300
+ }
6301
+ for (const line of text.split(/\r?\n/)) {
6302
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)?\s*$/);
6303
+ if (!match) {
6304
+ continue;
6305
+ }
6306
+ const [, key, rawValue = ""] = match;
6307
+ if (!key || process.env[key] !== void 0) {
6308
+ continue;
6309
+ }
6310
+ process.env[key] = parseEnvValue(rawValue);
6311
+ }
6312
+ }
6313
+ }
6314
+ function parseEnvValue(value) {
6315
+ const trimmed = value.trim();
6316
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
6317
+ return trimmed.slice(1, -1).replace(/\\n/g, "\n");
6318
+ }
6319
+ return trimmed.replace(/\s+#.*$/, "");
6320
+ }
5749
6321
  function readOptionalPlugins(argv) {
5750
6322
  if (argv.includes("--plugins")) {
5751
6323
  return true;
@@ -5755,9 +6327,87 @@ function readOptionalPlugins(argv) {
5755
6327
  }
5756
6328
  return void 0;
5757
6329
  }
6330
+ function renderBuildUrlSummary(options) {
6331
+ const mcpPath = options.mcpPath ?? "/mcp";
6332
+ const lines = ["Sidecar URLs:"];
6333
+ if (options.host === "vercel") {
6334
+ lines.push(` Vercel MCP route: https://<project>.vercel.app${mcpPath}`);
6335
+ } else {
6336
+ lines.push(` Local Node MCP: http://127.0.0.1:${options.port ?? "3101"}${mcpPath}`);
6337
+ }
6338
+ lines.push(` Public MCP: ${options.publicMcpUrl ?? `set SIDECAR_MCP_URL=https://your-host.example.com${mcpPath}`}`);
6339
+ lines.push(` Public base: ${options.publicUrl ?? "set SIDECAR_PUBLIC_URL=https://your-host.example.com when callbacks need a base URL"}`);
6340
+ return lines.join("\n");
6341
+ }
6342
+ function renderDevUrlSummary(options) {
6343
+ const stateUrl = `${options.harnessUrl}/__sidecar/dev/state`;
6344
+ const lines = [
6345
+ "Sidecar URLs:",
6346
+ ` Local MCP: ${options.localMcpUrl}`,
6347
+ ` Dev harness: ${options.harnessUrl}`,
6348
+ ` Dev harness state: ${stateUrl}`
6349
+ ];
6350
+ if (options.runtimeMcpUrl !== options.localMcpUrl) {
6351
+ lines.push(` Public MCP (${options.tunnelProvider ?? "tunnel"}): ${options.runtimeMcpUrl}`);
6352
+ lines.push(` ChatGPT/Claude connector URL: ${options.runtimeMcpUrl}`);
6353
+ } else {
6354
+ lines.push(` Harness connector URL: ${options.localMcpUrl}`);
6355
+ }
6356
+ return lines.join("\n");
6357
+ }
6358
+ async function previewProject(options) {
6359
+ const builds = [];
6360
+ for (const target of options.targets) {
6361
+ const outDir = `.sidecar/preview/${target}`;
6362
+ const manifest = await buildProject({ rootDir: options.rootDir, outDir, target });
6363
+ printDiagnostics(manifest.diagnostics ?? []);
6364
+ builds.push({ target, outDir, manifest });
6365
+ }
6366
+ const server = createServer3(async (request, response) => {
6367
+ try {
6368
+ const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
6369
+ if (url2.pathname === "/") {
6370
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
6371
+ response.end(renderProjectPreviewHtml(builds));
6372
+ return;
6373
+ }
6374
+ if (url2.pathname === "/widget") {
6375
+ const target = url2.searchParams.get("target");
6376
+ const tool = url2.searchParams.get("tool");
6377
+ const theme = url2.searchParams.get("theme") === "dark" ? "dark" : "light";
6378
+ const build = builds.find((entry2) => entry2.target === target);
6379
+ const entry = build?.manifest.tools.find((item) => item.id === tool);
6380
+ if (!build || !entry?.widget?.outputFile) {
6381
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
6382
+ response.end("Widget not found.");
6383
+ return;
6384
+ }
6385
+ const html = await readFile8(path21.join(options.rootDir, build.outDir, entry.widget.outputFile), "utf8");
6386
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
6387
+ response.end(injectProjectPreviewBridge(html, entry, build.target, theme));
6388
+ return;
6389
+ }
6390
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
6391
+ response.end("Not found.");
6392
+ } catch (error) {
6393
+ response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
6394
+ response.end(error instanceof Error ? error.message : String(error));
6395
+ }
6396
+ });
6397
+ const port = await listenOnAnyPort(server);
6398
+ const url = `http://127.0.0.1:${port}`;
6399
+ console.log(`Sidecar preview running at ${url}`);
6400
+ console.log(`Targets: ${options.targets.join(", ")}`);
6401
+ console.log("Press Ctrl+C to stop.");
6402
+ const shutdown = () => {
6403
+ server.close(() => exit(0));
6404
+ };
6405
+ process.once("SIGINT", shutdown);
6406
+ process.once("SIGTERM", shutdown);
6407
+ }
5758
6408
  async function previewComponents(options) {
5759
- const cliDir = path20.dirname(fileURLToPath(import.meta.url));
5760
- const css = await readFile8(path20.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path20.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
6409
+ const cliDir = path21.dirname(fileURLToPath2(import.meta.url));
6410
+ const css = await readFile8(path21.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path21.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
5761
6411
  const html = renderComponentPreviewHtml(
5762
6412
  options.host,
5763
6413
  options.compare,
@@ -5765,7 +6415,7 @@ async function previewComponents(options) {
5765
6415
  options.themes,
5766
6416
  options.componentSet
5767
6417
  );
5768
- const server = createServer2((_request, response) => {
6418
+ const server = createServer3((_request, response) => {
5769
6419
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
5770
6420
  response.end(html);
5771
6421
  });
@@ -5796,11 +6446,229 @@ async function previewComponents(options) {
5796
6446
  }
5797
6447
  server.close();
5798
6448
  }
6449
+ function renderProjectPreviewHtml(builds) {
6450
+ const themes = ["light", "dark"];
6451
+ const sections = themes.map((theme) => renderProjectPreviewTheme(builds, theme)).join("");
6452
+ const toolCount = new Set(
6453
+ builds.flatMap((build) => build.manifest.tools.filter((tool) => tool.widget).map((tool) => tool.id))
6454
+ ).size;
6455
+ return `<!doctype html>
6456
+ <html lang="en">
6457
+ <head>
6458
+ <meta charset="utf-8" />
6459
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6460
+ <title>Sidecar preview</title>
6461
+ <style>
6462
+ :root { color-scheme: light dark; }
6463
+ * { box-sizing: border-box; }
6464
+ body { background: #f4f4f5; color: #18181b; font: 14px/1.45 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; }
6465
+ header { align-items: baseline; background: rgb(255 255 255 / 92%); border-bottom: 1px solid rgb(0 0 0 / 10%); display: flex; gap: 14px; padding: 14px 18px; position: sticky; top: 0; z-index: 1; }
6466
+ h1 { font-size: 18px; margin: 0; }
6467
+ .summary { color: #5f5f66; }
6468
+ .theme-section { display: grid; gap: 12px; padding: 18px; }
6469
+ .theme-title { color: #3f3f46; font-size: 12px; font-weight: 800; letter-spacing: .05em; margin: 0; text-transform: uppercase; }
6470
+ .target-grid { align-items: start; display: grid; gap: 14px; grid-template-columns: repeat(${Math.max(builds.length, 1)}, minmax(320px, 1fr)); overflow-x: auto; padding-bottom: 8px; }
6471
+ .target-column { background: rgb(255 255 255 / 72%); border: 1px solid rgb(0 0 0 / 10%); border-radius: 10px; display: grid; gap: 12px; min-width: 320px; padding: 12px; }
6472
+ .target-title { font-size: 13px; font-weight: 800; margin: 0; text-transform: uppercase; }
6473
+ .widget-card { border: 1px solid rgb(0 0 0 / 10%); border-radius: 9px; display: grid; gap: 8px; overflow: hidden; }
6474
+ .widget-label { align-items: baseline; background: rgb(250 250 250 / 92%); border-bottom: 1px solid rgb(0 0 0 / 8%); display: flex; justify-content: space-between; padding: 8px 10px; }
6475
+ .widget-name { font-weight: 700; }
6476
+ .widget-id { color: #71717a; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; }
6477
+ iframe { background: transparent; border: 0; height: 360px; width: 100%; }
6478
+ .empty { border: 1px dashed rgb(0 0 0 / 18%); border-radius: 9px; color: #71717a; padding: 18px; }
6479
+ .theme-section[data-theme="light"] { background: #f4f4f5; color: #18181b; }
6480
+ .theme-section[data-theme="dark"] { background: #111113; color: #f4f4f5; }
6481
+ .theme-section[data-theme="dark"] .theme-title { color: #d4d4d8; }
6482
+ .theme-section[data-theme="dark"] .target-column,
6483
+ .theme-section[data-theme="dark"] .widget-card { background: rgb(24 24 27 / 72%); border-color: rgb(255 255 255 / 12%); }
6484
+ .theme-section[data-theme="dark"] .widget-label { background: rgb(39 39 42 / 72%); border-bottom-color: rgb(255 255 255 / 10%); }
6485
+ .theme-section[data-theme="dark"] .widget-id,
6486
+ .theme-section[data-theme="dark"] .empty { color: #a1a1aa; }
6487
+ </style>
6488
+ </head>
6489
+ <body>
6490
+ <header>
6491
+ <h1>Sidecar preview</h1>
6492
+ <span class="summary">${toolCount} widget${toolCount === 1 ? "" : "s"} across ${builds.length} target${builds.length === 1 ? "" : "s"}</span>
6493
+ </header>
6494
+ ${sections}
6495
+ </body>
6496
+ </html>`;
6497
+ }
6498
+ function renderProjectPreviewTheme(builds, theme) {
6499
+ const columns = builds.map((build) => renderProjectPreviewTarget(build, theme)).join("");
6500
+ return `<section class="theme-section" data-theme="${theme}">
6501
+ <h2 class="theme-title">${escapeHtml2(theme)}</h2>
6502
+ <div class="target-grid">${columns}</div>
6503
+ </section>`;
6504
+ }
6505
+ function renderProjectPreviewTarget(build, theme) {
6506
+ const widgets = build.manifest.tools.filter((tool) => tool.widget?.outputFile).map((tool) => renderProjectPreviewWidget(build.target, tool, theme)).join("");
6507
+ return `<section class="target-column">
6508
+ <h3 class="target-title">${escapeHtml2(previewTargetLabel(build.target))}</h3>
6509
+ ${widgets || `<div class="empty">No widgets found for ${escapeHtml2(build.target)}.</div>`}
6510
+ </section>`;
6511
+ }
6512
+ function renderProjectPreviewWidget(target, tool, theme) {
6513
+ const src = `/widget?target=${encodeURIComponent(target)}&tool=${encodeURIComponent(tool.id)}&theme=${theme}`;
6514
+ return `<article class="widget-card">
6515
+ <div class="widget-label">
6516
+ <span class="widget-name">${escapeHtml2(tool.name)}</span>
6517
+ <span class="widget-id">${escapeHtml2(tool.id)}</span>
6518
+ </div>
6519
+ <iframe title="${escapeHtml2(`${tool.name} ${target} ${theme}`)}" src="${escapeHtml2(src)}"></iframe>
6520
+ </article>`;
6521
+ }
6522
+ function injectProjectPreviewBridge(html, tool, target, theme) {
6523
+ const host = previewHostName(target);
6524
+ const preview = {
6525
+ hostContext: {
6526
+ name: host,
6527
+ theme,
6528
+ source: "mcp-apps",
6529
+ raw: {
6530
+ theme,
6531
+ userAgent: `Sidecar Preview ${previewTargetLabel(target)}`,
6532
+ displayMode: "inline",
6533
+ availableDisplayModes: ["inline", "fullscreen", "pip"]
6534
+ }
6535
+ },
6536
+ hostCapabilities: {
6537
+ openLinks: {},
6538
+ serverTools: {},
6539
+ serverResources: {},
6540
+ logging: {},
6541
+ message: { text: {}, structuredContent: {} },
6542
+ updateModelContext: { structuredContent: {} }
6543
+ },
6544
+ toolInput: {
6545
+ arguments: previewToolArguments(tool)
6546
+ },
6547
+ toolResult: previewToolResult(tool)
6548
+ };
6549
+ const script = `<script>window.__sidecarPreview=${escapeScriptJson(preview)};</script>`;
6550
+ return html.replace('<html lang="en"', `<html lang="en" data-sidecar-host="${host}" data-sidecar-theme="${theme}" data-theme="${theme}"`).replace("</head>", `${script}
6551
+ </head>`);
6552
+ }
6553
+ function previewToolResult(tool) {
6554
+ const content = [
6555
+ `# ${tool.name}`,
6556
+ "",
6557
+ tool.description,
6558
+ "",
6559
+ "| Field | Value |",
6560
+ "| --- | --- |",
6561
+ `| Tool | ${escapeMarkdownTableCell(tool.id)} |`,
6562
+ "| Preview | Ready |"
6563
+ ].join("\n");
6564
+ const structuredContent = {
6565
+ tool: tool.id,
6566
+ ok: true,
6567
+ text: content,
6568
+ preview: {
6569
+ kind: previewKind(tool),
6570
+ title: tool.name,
6571
+ summary: tool.description,
6572
+ content,
6573
+ items: [
6574
+ {
6575
+ title: `${tool.name} preview`,
6576
+ body: tool.description
6577
+ },
6578
+ {
6579
+ title: "Table rendering",
6580
+ body: "Preview data includes a Markdown table."
6581
+ }
6582
+ ]
6583
+ },
6584
+ upstream: {
6585
+ isError: false
6586
+ }
6587
+ };
6588
+ return {
6589
+ structuredContent,
6590
+ content: [{ type: "text", text: tool.description }],
6591
+ _meta: {
6592
+ sidecarPreview: true,
6593
+ tool: tool.id
6594
+ },
6595
+ isError: false
6596
+ };
6597
+ }
6598
+ function previewToolArguments(tool) {
6599
+ const properties = tool.descriptor.inputSchema?.properties;
6600
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
6601
+ return {};
6602
+ }
6603
+ return Object.fromEntries(
6604
+ Object.entries(properties).map(([key, schema]) => [key, previewValueForSchema(schema)])
6605
+ );
6606
+ }
6607
+ function previewValueForSchema(schema) {
6608
+ if (!schema || typeof schema !== "object") {
6609
+ return "preview";
6610
+ }
6611
+ const record = schema;
6612
+ if (Array.isArray(record.enum) && record.enum.length) {
6613
+ return record.enum[0];
6614
+ }
6615
+ switch (record.type) {
6616
+ case "number":
6617
+ case "integer":
6618
+ return 1;
6619
+ case "boolean":
6620
+ return true;
6621
+ case "array":
6622
+ return [];
6623
+ case "object":
6624
+ return {};
6625
+ default:
6626
+ return "preview";
6627
+ }
6628
+ }
6629
+ function previewKind(tool) {
6630
+ const id = tool.id.toLowerCase();
6631
+ if (id.includes("search") || id.includes("query")) {
6632
+ return "search";
6633
+ }
6634
+ if (id.includes("create") || id.includes("update") || id.includes("move") || id.includes("duplicate")) {
6635
+ return "write";
6636
+ }
6637
+ if (id.includes("fetch") || id.includes("read") || id.includes("get")) {
6638
+ return "read";
6639
+ }
6640
+ return "metadata";
6641
+ }
6642
+ function previewHostName(target) {
6643
+ if (target === "chatgpt") {
6644
+ return "chatgpt";
6645
+ }
6646
+ if (target === "claude") {
6647
+ return "claude";
6648
+ }
6649
+ return "generic";
6650
+ }
6651
+ function previewTargetLabel(target) {
6652
+ switch (target) {
6653
+ case "chatgpt":
6654
+ return "ChatGPT";
6655
+ case "claude":
6656
+ return "Claude";
6657
+ default:
6658
+ return "MCP";
6659
+ }
6660
+ }
6661
+ function escapeScriptJson(value) {
6662
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6663
+ }
6664
+ function escapeMarkdownTableCell(value) {
6665
+ return value.replace(/\|/g, "\\|");
6666
+ }
5799
6667
  async function writeComponentApproval(rootDir, approval) {
5800
- const dir = path20.join(rootDir, ".sidecar", "approvals");
6668
+ const dir = path21.join(rootDir, ".sidecar", "approvals");
5801
6669
  await mkdir10(dir, { recursive: true });
5802
6670
  await writeFile10(
5803
- path20.join(dir, `components.${approval.host}.json`),
6671
+ path21.join(dir, `components.${approval.host}.json`),
5804
6672
  `${JSON.stringify(approval, null, 2)}
5805
6673
  `
5806
6674
  );
@@ -6094,6 +6962,28 @@ function previewComponentNames(componentSet) {
6094
6962
  "TextLink"
6095
6963
  ];
6096
6964
  }
6965
+ function readPreviewTargets(argv) {
6966
+ const values = [];
6967
+ for (let index = 0; index < argv.length; index += 1) {
6968
+ if (argv[index] !== "--target") {
6969
+ continue;
6970
+ }
6971
+ const value = argv[index + 1];
6972
+ if (value) {
6973
+ values.push(...value.split(",").map((entry) => entry.trim()).filter(Boolean));
6974
+ }
6975
+ }
6976
+ if (!values.length) {
6977
+ return ["mcp", "chatgpt", "claude"];
6978
+ }
6979
+ const targets = values.map((target) => {
6980
+ if (target === "mcp" || target === "chatgpt" || target === "claude") {
6981
+ return target;
6982
+ }
6983
+ throw new Error(`Unsupported Sidecar target "${target}". Expected mcp, chatgpt, or claude.`);
6984
+ });
6985
+ return Array.from(new Set(targets));
6986
+ }
6097
6987
  function readPreviewComponentSet(value) {
6098
6988
  if (!value || value === "representative") {
6099
6989
  return "representative";
@@ -6151,174 +7041,85 @@ function printDiagnostics(diagnostics) {
6151
7041
  console.warn(formatDiagnostic(diagnostic));
6152
7042
  }
6153
7043
  }
6154
- async function loadRuntimeAuth(rootDir) {
6155
- const authPath = path20.join(rootDir, "auth.ts");
6156
- if (!existsSync7(authPath)) {
6157
- return void 0;
6158
- }
6159
- const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
6160
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6161
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6162
- const module = await tsImport2(pathToFileURL3(authPath).href, {
6163
- parentURL,
6164
- tsconfig
6165
- });
6166
- const defaultExport = unwrapRuntimeDefault2(module.default);
6167
- if (!isSidecarAuth(defaultExport)) {
6168
- throw new Error("auth.ts must default-export auth({ ... }) from sidecar-ai.");
6169
- }
6170
- return defaultExport;
6171
- }
6172
- async function loadRuntimeProxy(rootDir) {
6173
- const proxyPath = path20.join(rootDir, "proxy.ts");
6174
- if (!existsSync7(proxyPath)) {
6175
- return void 0;
6176
- }
6177
- const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
6178
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6179
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6180
- const module = await tsImport2(pathToFileURL3(proxyPath).href, {
6181
- parentURL,
6182
- tsconfig
7044
+ async function startBuiltMcpServer(options) {
7045
+ const rootDir = path21.resolve(options.rootDir);
7046
+ const entrypoint = path21.join(rootDir, options.outDir, SERVER_ENTRYPOINT);
7047
+ const child = spawn2(execPath, [entrypoint], {
7048
+ cwd: rootDir,
7049
+ env: {
7050
+ ...process.env,
7051
+ HOST: "127.0.0.1",
7052
+ PORT: String(options.port),
7053
+ SIDECAR_HOST: "127.0.0.1",
7054
+ SIDECAR_MCP_URL: options.mcpUrl,
7055
+ SIDECAR_PORT: String(options.port)
7056
+ },
7057
+ stdio: ["ignore", "pipe", "pipe"]
6183
7058
  });
6184
- const defaultExport = unwrapRuntimeDefault2(module.default);
6185
- if (!isSidecarProxy(defaultExport)) {
6186
- throw new Error("proxy.ts must default-export proxy({ ... }) from @sidecar-ai/server/proxy.");
7059
+ const output = [];
7060
+ child.stdout?.on("data", (chunk) => output.push(String(chunk)));
7061
+ child.stderr?.on("data", (chunk) => output.push(String(chunk)));
7062
+ try {
7063
+ await waitForBuiltMcpServer(child, options.port, output);
7064
+ } catch (error) {
7065
+ await stopBuiltMcpServer(child);
7066
+ throw error;
6187
7067
  }
6188
- return defaultExport;
7068
+ return child;
6189
7069
  }
6190
- async function loadRuntimeConfig(rootDir) {
6191
- const configPath = path20.join(rootDir, "sidecar.config.ts");
6192
- if (!existsSync7(configPath)) {
6193
- return void 0;
6194
- }
6195
- const parentURL = pathToFileURL3(configPath).href;
6196
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6197
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6198
- const module = await tsImport2(pathToFileURL3(configPath).href, {
6199
- parentURL,
6200
- tsconfig
6201
- });
6202
- const defaultExport = unwrapRuntimeDefault2(module.default);
6203
- if (!defaultExport || typeof defaultExport !== "object") {
6204
- throw new Error("sidecar.config.ts must default-export defineConfig({ ... }) from sidecar-ai.");
7070
+ async function stopBuiltMcpServer(child) {
7071
+ if (!child || child.exitCode !== null || child.signalCode !== null) {
7072
+ return;
6205
7073
  }
6206
- return defaultExport;
6207
- }
6208
- async function loadRuntimeTools(rootDir, entries) {
6209
- const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
6210
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6211
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6212
- const loaded = [];
6213
- for (const entry of entries) {
6214
- const sourcePath = path20.join(rootDir, entry.sourceFile);
6215
- const module = await tsImport2(pathToFileURL3(sourcePath).href, {
6216
- parentURL,
6217
- tsconfig
6218
- });
6219
- const defaultExport = unwrapRuntimeDefault2(module.default);
6220
- if (!isSidecarTool(defaultExport)) {
6221
- throw new Error(`${entry.sourceFile} did not default-export a Sidecar tool.`);
6222
- }
6223
- loaded.push({
6224
- sourceFile: entry.sourceFile,
6225
- tool: defaultExport,
6226
- descriptor: entry.descriptor
7074
+ child.kill("SIGTERM");
7075
+ await new Promise((resolve) => {
7076
+ const timeout = setTimeout(() => {
7077
+ child.kill("SIGKILL");
7078
+ resolve();
7079
+ }, 2e3);
7080
+ child.once("exit", () => {
7081
+ clearTimeout(timeout);
7082
+ resolve();
6227
7083
  });
6228
- }
6229
- return loaded;
6230
- }
6231
- function attachBuiltToolDescriptors(loadedTools, manifest) {
6232
- const descriptorsBySource = new Map(
6233
- manifest.tools.map((entry) => [entry.sourceFile, entry.descriptor])
6234
- );
6235
- return loadedTools.map(({ sourceFile, tool, descriptor }) => ({
6236
- tool,
6237
- descriptor: descriptorsBySource.get(sourceFile) ?? descriptor
6238
- }));
7084
+ });
6239
7085
  }
6240
- async function loadResources(rootDir, outDir, manifest) {
6241
- const resources = [];
6242
- for (const entry of manifest.tools) {
6243
- if (!entry.widget?.outputFile) {
6244
- continue;
7086
+ async function waitForBuiltMcpServer(child, port, output) {
7087
+ const deadline = Date.now() + 2e4;
7088
+ while (Date.now() < deadline) {
7089
+ if (child.exitCode !== null || child.signalCode !== null) {
7090
+ throw new Error(`Sidecar dev MCP process exited before startup.
7091
+ ${output.join("").trim()}`);
6245
7092
  }
6246
- const text = await readFile8(path20.join(rootDir, outDir, entry.widget.outputFile), "utf8");
6247
- resources.push({
6248
- uri: entry.widget.resourceUri,
6249
- name: entry.name,
6250
- description: entry.widget.options?.description,
6251
- mimeType: MCP_APP_RESOURCE_MIME_TYPE,
6252
- text,
6253
- _meta: entry.widget.resourceMeta
6254
- });
6255
- }
6256
- const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
6257
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6258
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6259
- for (const entry of manifest.resources) {
6260
- const sourcePath = path20.join(rootDir, entry.sourceFile);
6261
- const module = await tsImport2(pathToFileURL3(sourcePath).href, {
6262
- parentURL,
6263
- tsconfig
6264
- });
6265
- const defaultExport = unwrapRuntimeDefault2(module.default);
6266
- if (!isSidecarResource(defaultExport)) {
6267
- throw new Error(`${entry.sourceFile} did not default-export a Sidecar resource.`);
6268
- }
6269
- resources.push({
6270
- uri: entry.uri,
6271
- descriptor: entry.descriptor,
6272
- resource: defaultExport
6273
- });
6274
- }
6275
- return resources;
6276
- }
6277
- async function loadRuntimePrompts(rootDir, manifest) {
6278
- const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
6279
- const tsconfigPath = path20.join(rootDir, "tsconfig.json");
6280
- const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
6281
- const loaded = [];
6282
- for (const entry of manifest.prompts) {
6283
- const sourcePath = path20.join(rootDir, entry.sourceFile);
6284
- const module = await tsImport2(pathToFileURL3(sourcePath).href, {
6285
- parentURL,
6286
- tsconfig
6287
- });
6288
- const defaultExport = unwrapRuntimeDefault2(module.default);
6289
- if (!isSidecarPrompt(defaultExport)) {
6290
- throw new Error(`${entry.sourceFile} did not default-export a Sidecar prompt.`);
7093
+ try {
7094
+ await fetch(`http://127.0.0.1:${port}/mcp`, {
7095
+ method: "GET",
7096
+ headers: { accept: "application/json, text/event-stream" }
7097
+ });
7098
+ return;
7099
+ } catch {
7100
+ await new Promise((resolve) => setTimeout(resolve, 100));
6291
7101
  }
6292
- loaded.push({
6293
- prompt: defaultExport,
6294
- descriptor: entry.descriptor
6295
- });
6296
7102
  }
6297
- return loaded;
6298
- }
6299
- function unwrapRuntimeDefault2(value) {
6300
- if (value && typeof value === "object" && "default" in value && Object.keys(value).every((key) => key === "default" || key === "__esModule")) {
6301
- return unwrapRuntimeDefault2(value.default);
6302
- }
6303
- return value;
7103
+ throw new Error(`Timed out waiting for Sidecar dev MCP server on 127.0.0.1:${port}.
7104
+ ${output.join("").trim()}`);
6304
7105
  }
6305
- function listenOnLocalhost(server, port) {
7106
+ function listenOnAnyPort(server) {
6306
7107
  return new Promise((resolve, reject) => {
6307
7108
  const onError = (error) => {
6308
7109
  reject(error);
6309
7110
  };
6310
7111
  server.once("error", onError);
6311
- server.listen(port, "127.0.0.1", () => {
7112
+ server.listen(0, "127.0.0.1", () => {
6312
7113
  server.off("error", onError);
6313
- resolve();
7114
+ const address = server.address();
7115
+ if (!address) {
7116
+ reject(new Error("Preview server did not expose a bound address."));
7117
+ return;
7118
+ }
7119
+ resolve(address.port);
6314
7120
  });
6315
7121
  });
6316
7122
  }
6317
- function closeServer(server) {
6318
- return new Promise((resolve) => {
6319
- server.close(() => resolve());
6320
- });
6321
- }
6322
7123
  function readOption(argv, name) {
6323
7124
  const index = argv.indexOf(name);
6324
7125
  if (index === -1) {
@@ -6341,11 +7142,16 @@ if (isDirectRun()) {
6341
7142
  });
6342
7143
  }
6343
7144
  export {
7145
+ injectProjectPreviewBridge,
6344
7146
  main,
6345
7147
  previewComponentNames,
6346
7148
  readPreviewComponentSet,
7149
+ readPreviewTargets,
6347
7150
  readPreviewThemes,
7151
+ renderBuildUrlSummary,
6348
7152
  renderComponentPreviewFrame,
6349
- renderComponentPreviewHtml
7153
+ renderComponentPreviewHtml,
7154
+ renderDevUrlSummary,
7155
+ renderProjectPreviewHtml
6350
7156
  };
6351
7157
  //# sourceMappingURL=index.js.map