Skip to content

Commit 07a6dc6

Browse files
authored
fix(web): use daemon MCP install info in agent guide (#4865)
1 parent 49a1c7b commit 07a6dc6

4 files changed

Lines changed: 403 additions & 14 deletions

File tree

apps/web/src/components/UseEverywhereModal.tsx

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import { useT } from '../i18n';
1717
import { modalOverlay, modalContent } from '../motion';
1818
import type { Dict } from '../i18n/types';
1919
import {
20+
agentGuideSnippetUsesMcpInstallInfo,
2021
buildAgentGuideMarkdown,
22+
renderAgentGuideSnippetBody,
23+
type AgentGuideMcpInstallInfo,
2124
type AgentGuideOptions,
2225
} from './use-everywhere/agent-guide';
2326
import {
@@ -147,14 +150,43 @@ export function UseEverywhereGuidePanel({
147150
const [activeId, setActiveId] = useState<GuideSection['id']>('overview');
148151
const [guideCopy, setGuideCopy] = useState<CopyState>('idle');
149152
const [snippetCopy, setSnippetCopy] = useState<{ key: string; state: CopyState } | null>(null);
153+
const [mcpInstallInfo, setMcpInstallInfo] = useState<AgentGuideMcpInstallInfo | null>(null);
154+
const mcpInstallInfoRequestRef = useRef<Promise<AgentGuideMcpInstallInfo | null> | null>(null);
150155
const guideSections = useMemo(() => localizeGuideSections(t), [t]);
151156

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

159191
const fullGuide = useMemo(
160192
() => buildAgentGuideMarkdown(guideOptions),
@@ -177,7 +209,14 @@ export function UseEverywhereGuidePanel({
177209
}, [activeId, guideSections]);
178210

179211
async function onCopyGuide() {
180-
const state = await copyText(fullGuide);
212+
const installInfo = mcpInstallInfo ?? await loadMcpInstallInfo();
213+
if (installInfo && installInfo !== mcpInstallInfo) {
214+
setMcpInstallInfo(installInfo);
215+
}
216+
const guide = installInfo
217+
? buildAgentGuideMarkdown({ ...guideOptions, mcpInstallInfo: installInfo })
218+
: fullGuide;
219+
const state = await copyText(guide);
181220
setGuideCopy(state);
182221
if (state !== 'idle') {
183222
window.setTimeout(() => setGuideCopy('idle'), COPY_RESET_MS);
@@ -190,7 +229,17 @@ export function UseEverywhereGuidePanel({
190229
area: 'use_everywhere_tab',
191230
element: 'copy',
192231
});
193-
const text = applyDaemonUrl(snippet.body, daemonUrl);
232+
let installInfo = mcpInstallInfo;
233+
if (agentGuideSnippetUsesMcpInstallInfo(snippet) && !installInfo) {
234+
installInfo = await loadMcpInstallInfo();
235+
if (installInfo && installInfo !== mcpInstallInfo) {
236+
setMcpInstallInfo(installInfo);
237+
}
238+
}
239+
const text = renderAgentGuideSnippetBody(snippet, {
240+
daemonUrl,
241+
mcpInstallInfo: installInfo,
242+
});
194243
const state = await copyText(text);
195244
setSnippetCopy({ key, state });
196245
if (state !== 'idle') {
@@ -230,6 +279,7 @@ export function UseEverywhereGuidePanel({
230279
<SectionView
231280
section={activeSection}
232281
daemonUrl={daemonUrl}
282+
mcpInstallInfo={mcpInstallInfo}
233283
snippetCopy={snippetCopy}
234284
onCopySnippet={onCopySnippet}
235285
/>
@@ -283,6 +333,16 @@ export function UseEverywhereGuidePanel({
283333
);
284334
}
285335

336+
function isAgentGuideMcpInstallInfo(data: unknown): data is AgentGuideMcpInstallInfo {
337+
if (!data || typeof data !== 'object') return false;
338+
const candidate = data as Partial<AgentGuideMcpInstallInfo>;
339+
return (
340+
typeof candidate.command === 'string' &&
341+
Array.isArray(candidate.args) &&
342+
candidate.args.every((arg) => typeof arg === 'string')
343+
);
344+
}
345+
286346
async function copyText(text: string): Promise<CopyState> {
287347
if (typeof navigator === 'undefined' || !navigator.clipboard) {
288348
return 'failed';
@@ -298,13 +358,15 @@ async function copyText(text: string): Promise<CopyState> {
298358
interface SectionViewProps {
299359
section: GuideSection;
300360
daemonUrl: string | undefined;
361+
mcpInstallInfo: AgentGuideMcpInstallInfo | null;
301362
snippetCopy: { key: string; state: CopyState } | null;
302363
onCopySnippet: (key: string, snippet: CodeSnippet) => void;
303364
}
304365

305366
function SectionView({
306367
section,
307368
daemonUrl,
369+
mcpInstallInfo,
308370
snippetCopy,
309371
onCopySnippet,
310372
}: SectionViewProps) {
@@ -356,7 +418,12 @@ function SectionView({
356418
className="use-everywhere-snippet__pre"
357419
data-language={snippet.language}
358420
>
359-
<code>{applyDaemonUrl(snippet.body, daemonUrl)}</code>
421+
<code>
422+
{renderAgentGuideSnippetBody(snippet, {
423+
daemonUrl,
424+
mcpInstallInfo,
425+
})}
426+
</code>
360427
</pre>
361428
</div>
362429
);

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

Lines changed: 115 additions & 10 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,22 @@ 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+
39+
export interface AgentGuideSnippetRenderOptions {
40+
daemonUrl: string | undefined;
41+
mcpInstallInfo: AgentGuideMcpInstallInfo | null;
42+
}
43+
2744
const DEFAULT_DAEMON_URL = 'http://127.0.0.1:7456';
2845

2946
export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string {
3047
const daemonUrl = (options.daemonUrl ?? DEFAULT_DAEMON_URL).replace(/\/$/, '');
48+
const installInfo = normalizeMcpInstallInfo(options.mcpInstallInfo);
3149
const lines: string[] = [];
3250

3351
lines.push('# Open Design — agent setup guide');
@@ -56,12 +74,23 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
5674
lines.push('');
5775
lines.push(' If it 404s or times out, ask the user to run `pnpm tools-dev` (dev) or open the Open Design app (packaged).');
5876
lines.push('');
59-
lines.push('2. Detect available agent CLIs and confirm `od` is on PATH:');
77+
if (installInfo) {
78+
lines.push('2. Use this daemon-reported MCP server config. Do not replace it with a bare `od` command:');
79+
lines.push('');
80+
lines.push(' ```json');
81+
lines.push(indent(buildMcpServerConfigSnippet(installInfo), ' '));
82+
lines.push(' ```');
83+
lines.push('');
84+
lines.push(` This config came from \`${daemonUrl}/api/mcp/install-info\` and preserves the absolute command, args, and env needed by packaged installs.`);
85+
} else {
86+
lines.push('2. Detect available agent CLIs and confirm `od` is on PATH:');
87+
lines.push('');
88+
lines.push(' ```bash');
89+
lines.push(' od doctor');
90+
lines.push(' od status --json');
91+
lines.push(' ```');
92+
}
6093
lines.push('');
61-
lines.push(' ```bash');
62-
lines.push(' od doctor');
63-
lines.push(' od status --json');
64-
lines.push(' ```');
6594
if (options.cliHint) {
6695
lines.push('');
6796
lines.push(` The user reported \`od\` at: \`${options.cliHint}\``);
@@ -88,7 +117,7 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
88117
lines.push('');
89118

90119
for (const section of GUIDE_SECTIONS) {
91-
lines.push(...renderSection(section, daemonUrl));
120+
lines.push(...renderSection(section, daemonUrl, installInfo));
92121
}
93122

94123
lines.push('## Reference URLs');
@@ -109,7 +138,31 @@ export function buildAgentGuideMarkdown(options: AgentGuideOptions = {}): string
109138
return lines.join('\n');
110139
}
111140

112-
function renderSection(section: GuideSection, daemonUrl: string): string[] {
141+
export function renderAgentGuideSnippetBody(
142+
snippet: CodeSnippet,
143+
options: AgentGuideSnippetRenderOptions,
144+
): string {
145+
const daemonUrl = (options.daemonUrl ?? DEFAULT_DAEMON_URL).replace(/\/$/, '');
146+
return renderSnippetBody(
147+
snippet,
148+
daemonUrl,
149+
normalizeMcpInstallInfo(options.mcpInstallInfo),
150+
);
151+
}
152+
153+
export function agentGuideSnippetUsesMcpInstallInfo(snippet: CodeSnippet): boolean {
154+
return (
155+
snippet.language === 'json' &&
156+
snippet.body.includes('"mcpServers"') &&
157+
snippet.body.includes('"command": "od"')
158+
);
159+
}
160+
161+
function renderSection(
162+
section: GuideSection,
163+
daemonUrl: string,
164+
installInfo: AgentGuideMcpInstallInfo | null,
165+
): string[] {
113166
const lines: string[] = [];
114167
lines.push(`## ${substituteDaemonUrl(section.heading, daemonUrl)}`);
115168
lines.push('');
@@ -122,7 +175,7 @@ function renderSection(section: GuideSection, daemonUrl: string): string[] {
122175
lines.push('');
123176
}
124177
for (const snippet of section.snippets) {
125-
lines.push(...renderSnippet(snippet, daemonUrl));
178+
lines.push(...renderSnippet(snippet, daemonUrl, installInfo));
126179
}
127180
if (section.footer) {
128181
lines.push(`> ${substituteDaemonUrl(section.footer, daemonUrl)}`);
@@ -131,17 +184,69 @@ function renderSection(section: GuideSection, daemonUrl: string): string[] {
131184
return lines;
132185
}
133186

134-
function renderSnippet(snippet: CodeSnippet, daemonUrl: string): string[] {
187+
function renderSnippet(
188+
snippet: CodeSnippet,
189+
daemonUrl: string,
190+
installInfo: AgentGuideMcpInstallInfo | null,
191+
): string[] {
135192
const lines: string[] = [];
136193
lines.push(`### ${substituteDaemonUrl(snippet.label, daemonUrl)}`);
137194
lines.push('');
138195
lines.push('```' + snippet.language);
139-
lines.push(substituteDaemonUrl(snippet.body, daemonUrl));
196+
lines.push(renderSnippetBody(snippet, daemonUrl, installInfo));
140197
lines.push('```');
141198
lines.push('');
142199
return lines;
143200
}
144201

202+
function renderSnippetBody(
203+
snippet: CodeSnippet,
204+
daemonUrl: string,
205+
installInfo: AgentGuideMcpInstallInfo | null,
206+
): string {
207+
if (installInfo && agentGuideSnippetUsesMcpInstallInfo(snippet)) {
208+
return buildMcpServerConfigSnippet(installInfo);
209+
}
210+
return substituteDaemonUrl(snippet.body, daemonUrl);
211+
}
212+
145213
function substituteDaemonUrl(body: string, daemonUrl: string): string {
146214
return body.replace(/http:\/\/127\.0\.0\.1:7456/g, daemonUrl);
147215
}
216+
217+
function buildMcpServerConfigSnippet(info: AgentGuideMcpInstallInfo): string {
218+
const env = info.env && Object.keys(info.env).length > 0 ? info.env : undefined;
219+
return JSON.stringify(
220+
{
221+
mcpServers: {
222+
'open-design': {
223+
command: info.command,
224+
args: info.args,
225+
...(env ? { env } : {}),
226+
},
227+
},
228+
},
229+
null,
230+
2,
231+
);
232+
}
233+
234+
function normalizeMcpInstallInfo(
235+
info: AgentGuideMcpInstallInfo | null | undefined,
236+
): AgentGuideMcpInstallInfo | null {
237+
if (!info || typeof info.command !== 'string' || info.command.length === 0) return null;
238+
if (!Array.isArray(info.args) || !info.args.every((arg) => typeof arg === 'string')) return null;
239+
const env: Record<string, string> = {};
240+
for (const [key, value] of Object.entries(info.env ?? {})) {
241+
if (typeof value === 'string') env[key] = value;
242+
}
243+
return {
244+
command: info.command,
245+
args: info.args,
246+
...(Object.keys(env).length > 0 ? { env } : {}),
247+
};
248+
}
249+
250+
function indent(body: string, prefix: string): string {
251+
return body.split('\n').map((line) => `${prefix}${line}`).join('\n');
252+
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,51 @@ 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+
});
102+
103+
it('does not rewrite CLI snippets with POSIX env prefixes for Windows packaged installs', () => {
104+
const md = buildAgentGuideMarkdown({
105+
daemonUrl: 'http://127.0.0.1:7456',
106+
mcpInstallInfo: {
107+
command: 'C:\\Program Files\\Open Design\\Open Design.exe',
108+
args: [
109+
'C:\\Program Files\\Open Design\\resources\\app\\apps\\daemon\\dist\\cli.js',
110+
'mcp',
111+
],
112+
env: {
113+
ELECTRON_RUN_AS_NODE: '1',
114+
OD_DATA_DIR: 'C:\\Users\\Ada\\AppData\\Roaming\\Open Design',
115+
},
116+
},
117+
});
118+
119+
expect(md).toContain('"command": "C:\\\\Program Files\\\\Open Design\\\\Open Design.exe"');
120+
expect(md).toContain('"ELECTRON_RUN_AS_NODE": "1"');
121+
expect(md).toContain('od skills list --json');
122+
expect(md).not.toMatch(/^\s*ELECTRON_RUN_AS_NODE=1\s+OD_DATA_DIR=/m);
123+
});
77124
});

0 commit comments

Comments
 (0)