@@ -14,6 +14,12 @@ import { GUIDE_SECTIONS, type CodeSnippet, type GuideSection } from './sections'
1414export 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+
2739const DEFAULT_DAEMON_URL = 'http://127.0.0.1:7456' ;
2840
2941export 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 ( / ^ o d (? = \s ) / gm, cliCommand )
198+ . replace ( / \$ \( o d (? = \s ) / g, `$(${ cliCommand } ` ) ;
199+ }
200+
145201function substituteDaemonUrl ( body : string , daemonUrl : string ) : string {
146202 return body . replace ( / h t t p : \/ \/ 1 2 7 \. 0 \. 0 \. 1 : 7 4 5 6 / 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 - Z a - z _ ] [ A - Z a - z 0 - 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 - Z a - z 0 - 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+ }
0 commit comments