@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.d.ts +38 -1
- package/dist/index.js +2413 -1607
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
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
|
|
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 {
|
|
7
|
-
import
|
|
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
|
-
|
|
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:
|
|
3137
|
+
required: readBooleanProperty4(initializer, "required")
|
|
3050
3138
|
};
|
|
3051
3139
|
}
|
|
3052
3140
|
return void 0;
|
|
3053
3141
|
}
|
|
3054
|
-
function
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
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
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
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
|
|
3864
|
-
|
|
3865
|
-
|
|
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
|
|
3868
|
-
|
|
3869
|
-
|
|
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
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
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
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
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
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
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
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
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
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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
|
-
|
|
4120
|
-
|
|
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
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
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
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
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
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
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
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
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
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
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
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
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
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
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
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
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
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
}
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
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
|
-
|
|
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
|
|
4491
|
-
const
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
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
|
|
4499
|
-
const
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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
|
-
|
|
4695
|
+
sink.send("done", {});
|
|
4505
4696
|
} catch (error) {
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
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
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
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.
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
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
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
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
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
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
|
-
|
|
4595
|
-
env: process.env
|
|
4812
|
+
toolCalls: calls
|
|
4596
4813
|
};
|
|
4597
4814
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
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
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
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
|
|
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
|
-
|
|
4659
|
-
|
|
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
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
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
|
-
|
|
4673
|
-
|
|
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
|
|
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
|
|
4678
|
-
|
|
4679
|
-
|
|
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
|
|
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
|
|
4686
|
-
|
|
4687
|
-
|
|
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
|
|
4691
|
-
if (
|
|
4692
|
-
return
|
|
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
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4951
|
+
used.add(candidate);
|
|
4952
|
+
return candidate;
|
|
4953
|
+
}
|
|
4954
|
+
function parseToolArguments(value) {
|
|
4955
|
+
if (!value.trim()) {
|
|
4956
|
+
return {};
|
|
4697
4957
|
}
|
|
4698
|
-
|
|
4699
|
-
|
|
4958
|
+
const parsed = JSON.parse(value);
|
|
4959
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4960
|
+
return {};
|
|
4700
4961
|
}
|
|
4701
|
-
return
|
|
4962
|
+
return parsed;
|
|
4702
4963
|
}
|
|
4703
|
-
function
|
|
4704
|
-
|
|
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
|
|
4707
|
-
|
|
4708
|
-
|
|
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 (
|
|
4711
|
-
|
|
4993
|
+
if (body.theme === "light" || body.theme === "dark") {
|
|
4994
|
+
state.theme = body.theme;
|
|
4712
4995
|
}
|
|
4713
|
-
|
|
4714
|
-
|
|
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
|
|
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
|
-
|
|
4734
|
-
|
|
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
|
|
4738
|
-
|
|
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
|
|
4741
|
-
|
|
4742
|
-
|
|
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
|
|
4753
|
-
|
|
4754
|
-
|
|
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
|
|
4764
|
-
|
|
4765
|
-
|
|
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
|
|
4779
|
-
const
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
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
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
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
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
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
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
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
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
if (
|
|
4822
|
-
|
|
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
|
-
|
|
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
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
}
|
|
4852
|
-
return new RequestCancelledError();
|
|
5097
|
+
async function streamdownClientScript() {
|
|
5098
|
+
streamdownClientScriptPromise ??= buildStreamdownClientScript();
|
|
5099
|
+
return await streamdownClientScriptPromise;
|
|
4853
5100
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
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
|
|
5139
|
+
return output;
|
|
4863
5140
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
5141
|
+
var HttpError = class extends Error {
|
|
5142
|
+
constructor(status, message) {
|
|
5143
|
+
super(message);
|
|
5144
|
+
this.status = status;
|
|
5145
|
+
this.name = "HttpError";
|
|
4867
5146
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
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
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
}
|
|
5190
|
+
});
|
|
5191
|
+
document.addEventListener("keydown", (event) => {
|
|
5192
|
+
if (event.key === "Escape" && authModalEl.dataset.open === "true") {
|
|
5193
|
+
closeAuthModal();
|
|
4876
5194
|
}
|
|
4877
|
-
|
|
4878
|
-
|
|
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
|
-
|
|
4881
|
-
|
|
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
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
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
|
-
|
|
4890
|
-
|
|
4891
|
-
return `${path21} must be an object.`;
|
|
5248
|
+
if (message.method === "ui/notifications/initialized") {
|
|
5249
|
+
sendFrameContext(event.source);
|
|
5250
|
+
return;
|
|
4892
5251
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
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
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
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
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
}
|
|
4911
|
-
|
|
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
|
-
|
|
4921
|
-
|
|
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 (
|
|
4931
|
-
|
|
4932
|
-
|
|
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
|
-
|
|
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 (
|
|
4942
|
-
|
|
5283
|
+
if (message.id !== undefined) {
|
|
5284
|
+
respondError(event.source, message.id, -32601, "Unsupported dev harness method: " + message.method);
|
|
4943
5285
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
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
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
return
|
|
4987
|
-
|
|
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
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
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
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
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
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
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
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
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
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
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
|
|
5451
|
+
return json.result;
|
|
5035
5452
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
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
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
headers,
|
|
5053
|
-
|
|
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
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
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
|
-
|
|
5061
|
-
|
|
5062
|
-
throw new JsonRpcError(-32600, "Invalid JSON-RPC request.");
|
|
5550
|
+
if (workspaceDeviceEl) {
|
|
5551
|
+
workspaceDeviceEl.textContent = state.device === "mobile" ? "Mobile" : "Desktop";
|
|
5063
5552
|
}
|
|
5064
|
-
|
|
5065
|
-
|
|
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
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
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
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
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
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
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
|
-
|
|
5120
|
-
|
|
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
|
|
5645
|
+
const path22 = options.path ?? "/mcp";
|
|
5132
5646
|
if (provider === "cloudflared") {
|
|
5133
|
-
return startCloudflared(localUrl,
|
|
5647
|
+
return startCloudflared(localUrl, path22, options.timeoutMs);
|
|
5134
5648
|
}
|
|
5135
|
-
return startWrangler(localUrl,
|
|
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,
|
|
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,
|
|
5781
|
+
mcpUrl: appendPath(publicUrl, path22),
|
|
5268
5782
|
close() {
|
|
5269
5783
|
child.kill("SIGTERM");
|
|
5270
5784
|
}
|
|
5271
5785
|
};
|
|
5272
5786
|
}
|
|
5273
|
-
async function startWrangler(localUrl,
|
|
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,
|
|
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,
|
|
5543
|
-
return `${origin.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: ${
|
|
6085
|
+
console.log(`Vercel MCP function: ${path21.join(resolvedOutDir, VERCEL_ENTRYPOINT)}`);
|
|
5572
6086
|
} else {
|
|
5573
|
-
console.log(`Hostable MCP server: ${
|
|
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
|
-
|
|
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
|
|
5607
|
-
|
|
5608
|
-
const
|
|
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
|
|
5620
|
-
|
|
5621
|
-
|
|
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
|
|
5643
|
-
|
|
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:
|
|
6155
|
+
auth: existsSync8(path21.join(rootDir, "auth.ts"))
|
|
5648
6156
|
});
|
|
5649
6157
|
}
|
|
5650
|
-
console.log(`MCP running on Streamable HTTP (${target})
|
|
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
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
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
|
-
|
|
5659
|
-
|
|
5660
|
-
}
|
|
6179
|
+
await harness?.close();
|
|
6180
|
+
await stopBuiltMcpServer(mcpProcess);
|
|
5661
6181
|
throw error;
|
|
5662
6182
|
}
|
|
5663
6183
|
const shutdown = () => {
|
|
5664
6184
|
tunnel?.close();
|
|
5665
|
-
|
|
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
|
-
|
|
5687
|
-
|
|
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
|
|
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 =
|
|
5760
|
-
const css = await readFile8(
|
|
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 =
|
|
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 =
|
|
6668
|
+
const dir = path21.join(rootDir, ".sidecar", "approvals");
|
|
5801
6669
|
await mkdir10(dir, { recursive: true });
|
|
5802
6670
|
await writeFile10(
|
|
5803
|
-
|
|
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
|
|
6155
|
-
const
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
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
|
|
6185
|
-
|
|
6186
|
-
|
|
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
|
|
7068
|
+
return child;
|
|
6189
7069
|
}
|
|
6190
|
-
async function
|
|
6191
|
-
|
|
6192
|
-
|
|
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
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
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
|
|
6241
|
-
const
|
|
6242
|
-
|
|
6243
|
-
if (
|
|
6244
|
-
|
|
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
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
7112
|
+
server.listen(0, "127.0.0.1", () => {
|
|
6312
7113
|
server.off("error", onError);
|
|
6313
|
-
|
|
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
|