Skip to content

Commit 73f2d04

Browse files
committed
fix(web): use daemon MCP install info in agent guide
1 parent 23704a9 commit 73f2d04

4 files changed

Lines changed: 304 additions & 13 deletions

File tree

apps/web/src/components/UseEverywhereModal.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { modalOverlay, modalContent } from '../motion';
1818
import type { Dict } from '../i18n/types';
1919
import {
2020
buildAgentGuideMarkdown,
21+
type AgentGuideMcpInstallInfo,
2122
type AgentGuideOptions,
2223
} from './use-everywhere/agent-guide';
2324
import {
@@ -147,14 +148,43 @@ export function UseEverywhereGuidePanel({
147148
const [activeId, setActiveId] = useState<GuideSection['id']>('overview');
148149
const [guideCopy, setGuideCopy] = useState<CopyState>('idle');
149150
const [snippetCopy, setSnippetCopy] = useState<{ key: string; state: CopyState } | null>(null);
151+
const [mcpInstallInfo, setMcpInstallInfo] = useState<AgentGuideMcpInstallInfo | null>(null);
152+
const mcpInstallInfoRequestRef = useRef<Promise<AgentGuideMcpInstallInfo | null> | null>(null);
150153
const guideSections = useMemo(() => localizeGuideSections(t), [t]);
151154

155+
function loadMcpInstallInfo(): Promise<AgentGuideMcpInstallInfo | null> {
156+
if (!mcpInstallInfoRequestRef.current) {
157+
mcpInstallInfoRequestRef.current = fetch('/api/mcp/install-info')
158+
.then(async (res) => {
159+
if (!res.ok) throw new Error(`daemon ${res.status}`);
160+
const data = (await res.json()) as unknown;
161+
if (isAgentGuideMcpInstallInfo(data)) return data;
162+
return null;
163+
})
164+
.catch(() => null);
165+
}
166+
return mcpInstallInfoRequestRef.current;
167+
}
168+
169+
useEffect(() => {
170+
let cancelled = false;
171+
loadMcpInstallInfo()
172+
.then((data) => {
173+
if (cancelled) return;
174+
setMcpInstallInfo(data);
175+
});
176+
return () => {
177+
cancelled = true;
178+
};
179+
}, []);
180+
152181
const guideOptions: AgentGuideOptions = useMemo(() => {
153182
const opts: AgentGuideOptions = {};
154183
if (daemonUrl) opts.daemonUrl = daemonUrl;
155184
if (versionHint) opts.versionHint = versionHint;
185+
if (mcpInstallInfo) opts.mcpInstallInfo = mcpInstallInfo;
156186
return opts;
157-
}, [daemonUrl, versionHint]);
187+
}, [daemonUrl, mcpInstallInfo, versionHint]);
158188

159189
const fullGuide = useMemo(
160190
() => buildAgentGuideMarkdown(guideOptions),
@@ -177,7 +207,14 @@ export function UseEverywhereGuidePanel({
177207
}, [activeId, guideSections]);
178208

179209
async function onCopyGuide() {
180-
const state = await copyText(fullGuide);
210+
const installInfo = mcpInstallInfo ?? await loadMcpInstallInfo();
211+
if (installInfo && installInfo !== mcpInstallInfo) {
212+
setMcpInstallInfo(installInfo);
213+
}
214+
const guide = installInfo
215+
? buildAgentGuideMarkdown({ ...guideOptions, mcpInstallInfo: installInfo })
216+
: fullGuide;
217+
const state = await copyText(guide);
181218
setGuideCopy(state);
182219
if (state !== 'idle') {
183220
window.setTimeout(() => setGuideCopy('idle'), COPY_RESET_MS);
@@ -283,6 +320,16 @@ export function UseEverywhereGuidePanel({
283320
);
284321
}
285322

323+
function isAgentGuideMcpInstallInfo(data: unknown): data is AgentGuideMcpInstallInfo {
324+
if (!data || typeof data !== 'object') return false;
325+
const candidate = data as Partial<AgentGuideMcpInstallInfo>;
326+
return (
327+
typeof candidate.command === 'string' &&
328+
Array.isArray(candidate.args) &&
329+
candidate.args.every((arg) => typeof arg === 'string')
330+
);
331+
}
332+
286333
async function copyText(text: string): Promise<CopyState> {
287334
if (typeof navigator === 'undefined' || !navigator.clipboard) {
288335
return 'failed';

apps/web/src/components/use-everywhere/agent-guide.ts

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import { GUIDE_SECTIONS, type CodeSnippet, type GuideSection } from './sections'
1414
export interface AgentGuideOptions {
1515
/** Live daemon URL detected at modal-open time. Defaults to the documented port. */
1616
daemonUrl?: string;
17+
/**
18+
* Launch spec returned by `/api/mcp/install-info`. When present, MCP
19+
* snippets use this absolute command/args/env tuple instead of assuming
20+
* an `od` binary exists on PATH.
21+
*/
22+
mcpInstallInfo?: AgentGuideMcpInstallInfo | null;
1723
/**
1824
* Optional `od` binary path / hint. When provided we mention it in the
1925
* setup checklist so the agent knows whether to run `od …` directly or
@@ -24,10 +30,18 @@ export interface AgentGuideOptions {
2430
versionHint?: string;
2531
}
2632

33+
export interface AgentGuideMcpInstallInfo {
34+
command: string;
35+
args: string[];
36+
env?: Record<string, string>;
37+
}
38+
2739
const DEFAULT_DAEMON_URL = 'http://127.0.0.1:7456';
2840

2941
export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string {
3042
const daemonUrl = (options.daemonUrl ?? DEFAULT_DAEMON_URL).replace(/\/$/, '');
43+
const installInfo = normalizeMcpInstallInfo(options.mcpInstallInfo);
44+
const cliCommand = installInfo ? renderCliCommandPrefix(installInfo) : null;
3145
const lines: string[] = [];
3246

3347
lines.push('# Open Design — agent setup guide');
@@ -56,12 +70,23 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
5670
lines.push('');
5771
lines.push(' If it 404s or times out, ask the user to run `pnpm tools-dev` (dev) or open the Open Design app (packaged).');
5872
lines.push('');
59-
lines.push('2. Detect available agent CLIs and confirm `od` is on PATH:');
73+
if (installInfo) {
74+
lines.push('2. Use this daemon-reported MCP server config. Do not replace it with a bare `od` command:');
75+
lines.push('');
76+
lines.push(' ```json');
77+
lines.push(indent(buildMcpServerConfigSnippet(installInfo), ' '));
78+
lines.push(' ```');
79+
lines.push('');
80+
lines.push(` This config came from \`${daemonUrl}/api/mcp/install-info\` and preserves the absolute command, args, and env needed by packaged installs.`);
81+
} else {
82+
lines.push('2. Detect available agent CLIs and confirm `od` is on PATH:');
83+
lines.push('');
84+
lines.push(' ```bash');
85+
lines.push(' od doctor');
86+
lines.push(' od status --json');
87+
lines.push(' ```');
88+
}
6089
lines.push('');
61-
lines.push(' ```bash');
62-
lines.push(' od doctor');
63-
lines.push(' od status --json');
64-
lines.push(' ```');
6590
if (options.cliHint) {
6691
lines.push('');
6792
lines.push(` The user reported \`od\` at: \`${options.cliHint}\``);
@@ -83,12 +108,12 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
83108
lines.push('');
84109
lines.push(' ```bash');
85110
lines.push(` curl -s ${daemonUrl}/api/skills | jq '.skills | length'`);
86-
lines.push(' od skills list --json');
111+
lines.push(` ${cliCommand ?? 'od'} skills list --json`);
87112
lines.push(' ```');
88113
lines.push('');
89114

90115
for (const section of GUIDE_SECTIONS) {
91-
lines.push(...renderSection(section, daemonUrl));
116+
lines.push(...renderSection(section, daemonUrl, installInfo, cliCommand));
92117
}
93118

94119
lines.push('## Reference URLs');
@@ -109,7 +134,12 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
109134
return lines.join('\n');
110135
}
111136

112-
function renderSection(section: GuideSection, daemonUrl: string): string[] {
137+
function renderSection(
138+
section: GuideSection,
139+
daemonUrl: string,
140+
installInfo: AgentGuideMcpInstallInfo | null,
141+
cliCommand: string | null,
142+
): string[] {
113143
const lines: string[] = [];
114144
lines.push(`## ${substituteDaemonUrl(section.heading, daemonUrl)}`);
115145
lines.push('');
@@ -122,7 +152,7 @@ function renderSection(section: GuideSection, daemonUrl: string): string[] {
122152
lines.push('');
123153
}
124154
for (const snippet of section.snippets) {
125-
lines.push(...renderSnippet(snippet, daemonUrl));
155+
lines.push(...renderSnippet(snippet, daemonUrl, installInfo, cliCommand));
126156
}
127157
if (section.footer) {
128158
lines.push(`> ${substituteDaemonUrl(section.footer, daemonUrl)}`);
@@ -131,17 +161,97 @@ function renderSection(section: GuideSection, daemonUrl: string): string[] {
131161
return lines;
132162
}
133163

134-
function renderSnippet(snippet: CodeSnippet, daemonUrl: string): string[] {
164+
function renderSnippet(
165+
snippet: CodeSnippet,
166+
daemonUrl: string,
167+
installInfo: AgentGuideMcpInstallInfo | null,
168+
cliCommand: string | null,
169+
): string[] {
135170
const lines: string[] = [];
136171
lines.push(`### ${substituteDaemonUrl(snippet.label, daemonUrl)}`);
137172
lines.push('');
138173
lines.push('```' + snippet.language);
139-
lines.push(substituteDaemonUrl(snippet.body, daemonUrl));
174+
lines.push(renderSnippetBody(snippet, daemonUrl, installInfo, cliCommand));
140175
lines.push('```');
141176
lines.push('');
142177
return lines;
143178
}
144179

180+
function renderSnippetBody(
181+
snippet: CodeSnippet,
182+
daemonUrl: string,
183+
installInfo: AgentGuideMcpInstallInfo | null,
184+
cliCommand: string | null,
185+
): string {
186+
if (
187+
installInfo &&
188+
snippet.language === 'json' &&
189+
snippet.body.includes('"mcpServers"') &&
190+
snippet.body.includes('"command": "od"')
191+
) {
192+
return buildMcpServerConfigSnippet(installInfo);
193+
}
194+
const withUrl = substituteDaemonUrl(snippet.body, daemonUrl);
195+
if (!cliCommand) return withUrl;
196+
return withUrl
197+
.replace(/^od(?=\s)/gm, cliCommand)
198+
.replace(/\$\(od(?=\s)/g, `$(${cliCommand}`);
199+
}
200+
145201
function substituteDaemonUrl(body: string, daemonUrl: string): string {
146202
return body.replace(/http:\/\/127\.0\.0\.1:7456/g, daemonUrl);
147203
}
204+
205+
function buildMcpServerConfigSnippet(info: AgentGuideMcpInstallInfo): string {
206+
const env = info.env && Object.keys(info.env).length > 0 ? info.env : undefined;
207+
return JSON.stringify(
208+
{
209+
mcpServers: {
210+
'open-design': {
211+
command: info.command,
212+
args: info.args,
213+
...(env ? { env } : {}),
214+
},
215+
},
216+
},
217+
null,
218+
2,
219+
);
220+
}
221+
222+
function normalizeMcpInstallInfo(
223+
info: AgentGuideMcpInstallInfo | null | undefined,
224+
): AgentGuideMcpInstallInfo | null {
225+
if (!info || typeof info.command !== 'string' || info.command.length === 0) return null;
226+
if (!Array.isArray(info.args) || !info.args.every((arg) => typeof arg === 'string')) return null;
227+
const env: Record<string, string> = {};
228+
for (const [key, value] of Object.entries(info.env ?? {})) {
229+
if (typeof value === 'string') env[key] = value;
230+
}
231+
return {
232+
command: info.command,
233+
args: info.args,
234+
...(Object.keys(env).length > 0 ? { env } : {}),
235+
};
236+
}
237+
238+
function renderCliCommandPrefix(info: AgentGuideMcpInstallInfo): string {
239+
const mcpArgIndex = info.args.indexOf('mcp');
240+
const cliArgs = mcpArgIndex >= 0 ? info.args.slice(0, mcpArgIndex) : info.args;
241+
const envPrefix = Object.entries(info.env ?? {})
242+
.filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
243+
.map(([key, value]) => `${key}=${shellQuote(value)}`);
244+
return [...envPrefix, info.command, ...cliArgs].map((part, index) => {
245+
if (index < envPrefix.length) return part;
246+
return shellQuote(part);
247+
}).join(' ');
248+
}
249+
250+
function shellQuote(value: string): string {
251+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
252+
return `"${value.replace(/(["$`])/g, '\\$1')}"`;
253+
}
254+
255+
function indent(body: string, prefix: string): string {
256+
return body.split('\n').map((line) => `${prefix}${line}`).join('\n');
257+
}

apps/web/tests/components/use-everywhere-agent-guide.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,29 @@ describe('buildAgentGuideMarkdown', () => {
7474
expect(md).toContain('- Daemon: `http://example.test:5555`');
7575
expect(md).toContain('- MCP install info: `http://example.test:5555/api/mcp/install-info`');
7676
});
77+
78+
it('uses daemon install-info for the MCP config instead of assuming od is on PATH', () => {
79+
const md = buildAgentGuideMarkdown({
80+
daemonUrl: 'http://127.0.0.1:7456',
81+
mcpInstallInfo: {
82+
command: 'C:\\Program Files\\Open Design\\Open Design.exe',
83+
args: [
84+
'C:\\Program Files\\Open Design\\resources\\app\\apps\\daemon\\dist\\cli.js',
85+
'mcp',
86+
],
87+
env: {
88+
ELECTRON_RUN_AS_NODE: '1',
89+
OD_DATA_DIR: 'C:\\Users\\Ada\\AppData\\Roaming\\Open Design',
90+
},
91+
},
92+
});
93+
94+
expect(md).toContain('"command": "C:\\\\Program Files\\\\Open Design\\\\Open Design.exe"');
95+
expect(md).toContain(
96+
'"C:\\\\Program Files\\\\Open Design\\\\resources\\\\app\\\\apps\\\\daemon\\\\dist\\\\cli.js"',
97+
);
98+
expect(md).toContain('"ELECTRON_RUN_AS_NODE": "1"');
99+
expect(md).toContain('"OD_DATA_DIR": "C:\\\\Users\\\\Ada\\\\AppData\\\\Roaming\\\\Open Design"');
100+
expect(md).not.toContain('"command": "od"');
101+
});
77102
});

0 commit comments

Comments
 (0)