11import * as fs from 'fs' ;
22import * as path from 'path' ;
3- import { generatePortFromDirectory } from '../utils/port.js' ;
4- import { readConfig , resolveConnectionFromConfig } from '../utils/config.js' ;
53import {
6- getAgentById ,
4+ setupMcp as coreSetupMcp ,
5+ unityAdapter ,
76 getAgentIds ,
8- resolveServerBinaryPath ,
9- writeJsonAgentConfig ,
10- writeTomlAgentConfig ,
11- MCP_SERVER_NAME ,
12- } from '../utils/agents.js' ;
13- import { emitProgress } from './progress.js' ;
7+ } from '@baizor/gamedev-cli-core' ;
148import { requireExistingPath } from './validation.js' ;
159import type {
1610 SetupMcpOptions ,
@@ -23,199 +17,102 @@ function isValidTransport(t: string | undefined): t is McpTransport {
2317}
2418
2519/**
26- * Extract a port from a `host` URL string, falling back to the
27- * project-directory-derived deterministic port on any parse failure.
28- */
29- function portFromHost ( host : string | undefined , projectPath : string ) : number {
30- if ( ! host ) return generatePortFromDirectory ( projectPath ) ;
31- try {
32- return parseInt ( new URL ( host ) . port , 10 ) || generatePortFromDirectory ( projectPath ) ;
33- } catch {
34- return generatePortFromDirectory ( projectPath ) ;
35- }
36- }
37-
38- /**
39- * True when `configPath` resolves to a location at or under `projectRoot`
40- * (separator- and case-insensitive) — i.e. a VCS-visible project-scoped file.
41- * Mirrors the b6 configurator's `IsProjectScopedPath`, used to decide whether a
42- * written access token would land in a committable file (design 03 Flow C).
43- */
44- function isProjectScoped ( configPath : string , projectRoot : string ) : boolean {
45- const norm = ( p : string ) : string =>
46- path . resolve ( p ) . replace ( / \\ / g, '/' ) . replace ( / \/ + $ / , '' ) . toLowerCase ( ) ;
47- const root = norm ( projectRoot ) ;
48- const target = norm ( configPath ) ;
49- return target === root || target . startsWith ( root + '/' ) ;
50- }
51-
52- /**
53- * Write MCP configuration for the given AI agent, so it can talk to
54- * Unity-MCP. Library-safe: no stdout noise, no process.exit, no throws
55- * past the public boundary.
20+ * Write MCP configuration for the given AI agent, so it can talk to the Unity MCP hub.
5621 *
57- * The returned `SetupMcpResult` is a discriminated union — narrow with
58- * `result.kind === 'success'` to access `agentId` / `configPath` /
59- * `transport`, or `result.kind === 'failure'` to access `error`.
22+ * This is now a thin adapter over `@baizor/gamedev-cli-core`'s `setupMcp` (auth-fixes T4/M7/M8):
23+ * - the http URL is **pinned by default** to `<base>/mcp/p/<pin-v2>` and the stdio config carries a
24+ * `project=<pin>` arg, so the config routes strictly to this project's engine instance even when
25+ * the account has several (`--no-pin` is the escape hatch);
26+ * - the config is **credential-free by default** — a static `Authorization: Bearer` header (http)
27+ * or `token=` arg (stdio) is written ONLY on an explicit `--token` PAT opt-in, so an OAuth-capable
28+ * client authorizes natively (RFC 9728) instead of being suppressed by a static header;
29+ * - the config bytes are written by cli-core's golden-vector-gated JSON/TOML writers, so `setup-mcp`
30+ * output matches the Unity Editor's Configure output byte-for-byte.
31+ *
32+ * Library-safe: no stdout noise, no `process.exit`, no throws past the public boundary. Narrow the
33+ * returned `SetupMcpResult` with `result.kind === 'success'` / `=== 'failure'`.
6034 */
6135export async function setupMcp ( opts : SetupMcpOptions ) : Promise < SetupMcpResult > {
62- const warnings : string [ ] = [ ] ;
63- const nextSteps : string [ ] = [ ] ;
64-
36+ // This wrapper never accumulates its own warnings/next-steps — cli-core owns the warning channel
37+ // (`result.warnings`), and ` nextSteps` is always empty here. Both fields are written as literals
38+ // on each return to satisfy the `SetupMcpResult` shape.
6539 try {
66- if ( ! opts || typeof opts . agentId !== 'string' || opts . agentId . length === 0 ) {
67- return {
68- kind : 'failure' ,
69- success : false ,
70- warnings,
71- nextSteps,
72- error : new Error (
73- `agentId is required. Available agent IDs: ${ getAgentIds ( ) . join ( ', ' ) } ` ,
74- ) ,
75- } ;
40+ // Resolve project path. If the caller supplied one explicitly it must exist; otherwise cli-core
41+ // falls back to cwd (matching the CLI behaviour and closing the "path required" half of B1).
42+ let projectPath : string | undefined ;
43+ if ( opts . unityProjectPath ) {
44+ const validated = requireExistingPath ( opts . unityProjectPath ) ;
45+ if ( ! validated . ok ) {
46+ return { kind : 'failure' , success : false , warnings : [ ] , nextSteps : [ ] , error : validated . error } ;
47+ }
48+ projectPath = validated . projectPath ;
7649 }
7750
78- const agent = getAgentById ( opts . agentId ) ;
79- if ( ! agent ) {
51+ const transport = opts . transport ?? 'http' ;
52+ if ( ! isValidTransport ( transport ) ) {
8053 return {
8154 kind : 'failure' ,
8255 success : false ,
83- warnings,
84- nextSteps,
85- error : new Error (
86- `Unknown agent: "${ opts . agentId } ". Available agent IDs: ${ getAgentIds ( ) . join ( ', ' ) } ` ,
87- ) ,
56+ warnings : [ ] ,
57+ nextSteps : [ ] ,
58+ error : new Error ( `Invalid transport: "${ transport } ". Must be "stdio" or "http".` ) ,
8859 } ;
8960 }
9061
91- const transport : McpTransport = opts . transport ?? 'http' ;
92- if ( ! isValidTransport ( transport ) ) {
62+ const result = coreSetupMcp ( {
63+ adapter : unityAdapter ,
64+ agentId : opts . agentId ,
65+ transport,
66+ projectPath,
67+ url : opts . url ,
68+ token : opts . token ,
69+ noPin : opts . noPin === true ,
70+ } ) ;
71+
72+ if ( result . kind === 'failure' ) {
9373 return {
9474 kind : 'failure' ,
9575 success : false ,
96- warnings,
97- nextSteps,
98- error : new Error ( `Invalid transport: " ${ transport } ". Must be "stdio" or "http".` ) ,
76+ warnings : result . warnings ,
77+ nextSteps : [ ] ,
78+ error : result . error ,
9979 } ;
10080 }
10181
102- // Resolve project path. If the caller supplied one explicitly it
103- // must exist; otherwise fall back to cwd (matches CLI behaviour).
104- let projectPath : string ;
105- if ( opts . unityProjectPath ) {
106- const validated = requireExistingPath ( opts . unityProjectPath ) ;
107- if ( ! validated . ok ) {
108- return { kind : 'failure' , success : false , warnings, nextSteps, error : validated . error } ;
109- }
110- projectPath = validated . projectPath ;
111- } else {
112- projectPath = path . resolve ( process . cwd ( ) ) ;
113- }
114-
115- emitProgress ( opts . onProgress , {
116- phase : 'start' ,
117- message : `Configuring ${ agent . name } (${ transport } ) for ${ projectPath } ` ,
118- } ) ;
119-
120- const config = readConfig ( projectPath ) ;
121- const fromConfig = config
122- ? resolveConnectionFromConfig ( config )
123- : { url : undefined , token : undefined } ;
124-
125- const port = portFromHost ( config ?. host , projectPath ) ;
126-
127- const timeout = ( config ?. timeoutMs as number ) ?? 10000 ;
128- const auth = ( config ?. authOption as string ) ?? 'none' ;
129-
130- // Explicit PAT opt-in (design 03 Flow C): a token the caller passed on the
131- // command line (`--token`). Only an explicit opt-in — never a token merely
132- // resolved from the project config — places a static credential in the file.
133- const patOptIn = typeof opts . token === 'string' && opts . token . length > 0 ;
134- const token = opts . token ?? fromConfig . token ?? '' ;
135-
136- // b6 credential policy (design decision D11): the DEFAULT http config is
137- // credential-free. OAuth-capable clients perform native RFC 9728 OAuth
138- // against the URL (Flow A), so a static `Authorization: Bearer` header both
139- // FAILS (the hosted ai-game.dev/mcp endpoint 401s the token) AND suppresses
140- // the client's own OAuth handshake ("OAuth fallback is disabled when
141- // headers.Authorization is set"). A static header is written ONLY for an
142- // explicit PAT opt-in (Flow C) or a client that cannot do MCP OAuth
143- // (`supportsOAuth === false`) — never automatically for an OAuth-capable
144- // client, regardless of the server's `authRequired` setting.
145- const supportsOAuth = agent . supportsOAuth !== false ;
146- const emitAuthHeader = token . length > 0 && ( patOptIn || ! supportsOAuth ) ;
147-
148- const serverPath = resolveServerBinaryPath ( projectPath ) . replace ( / \\ / g, '/' ) ;
149-
150- // Resolve URL for HTTP — explicit override, then config, then
151- // deterministic localhost fallback. Trailing slash stripped.
152- const serverUrl = ( opts . url ?? fromConfig . url ?? `http://localhost:${ port } ` ) . replace ( / \/ $ / , '' ) ;
153-
154- const configPath = agent . getConfigPath ( projectPath ) ;
155-
156- let props : Record < string , unknown > ;
157- let removeKeys : string [ ] ;
158-
82+ // Preserve the historical stdio-server-missing warning (cli-core does not emit it): the plugin
83+ // downloads the server binary when Unity opens, so a stdio config written before that is valid
84+ // but not yet runnable.
15985 if ( transport === 'stdio' ) {
160- props = agent . getStdioProps ( serverPath , port , timeout , auth , token ) ;
161- removeKeys = agent . stdioRemoveKeys ;
162- } else {
163- props = agent . getHttpProps ( serverUrl , token , emitAuthHeader ) ;
164- removeKeys = agent . httpRemoveKeys ;
165- }
166-
167- if ( agent . configFormat === 'toml' ) {
168- writeTomlAgentConfig ( configPath , agent . bodyPath , MCP_SERVER_NAME , props , removeKeys ) ;
169- } else {
170- writeJsonAgentConfig ( configPath , agent . bodyPath , MCP_SERVER_NAME , props , removeKeys ) ;
171- }
172-
173- emitProgress ( opts . onProgress , {
174- phase : 'manifest-patched' ,
175- message : `Wrote ${ configPath } ` ,
176- manifestPath : configPath ,
177- } ) ;
178-
179- // Flow C credential-placement rule (design 03): a static access token
180- // written into a project-scoped config file is VCS-visible. Warn so the
181- // user prefers an env-var / user-scope placement for the credential.
182- if ( transport === 'http' && emitAuthHeader && isProjectScoped ( configPath , projectPath ) ) {
183- warnings . push (
184- `Wrote an access token into project-scoped config file "${ configPath } " — it is under the project root and may be committed to version control. Prefer an env-var or user-scope placement for the access token (design 03 Flow C).` ,
185- ) ;
186- }
187-
188- if ( transport === 'stdio' && ! fs . existsSync ( serverPath . replace ( / \/ / g, path . sep ) ) ) {
189- warnings . push (
190- 'Server binary not found. Open Unity with the MCP plugin to download it automatically.' ,
191- ) ;
86+ const serverPath = unityAdapter . serverBinaryPath ( projectPath ?? path . resolve ( process . cwd ( ) ) ) ;
87+ if ( ! fs . existsSync ( serverPath ) ) {
88+ result . warnings . push (
89+ 'Server binary not found. Open Unity with the MCP plugin to download it automatically.' ,
90+ ) ;
91+ }
19292 }
19393
194- emitProgress ( opts . onProgress , { phase : 'done' , message : `${ agent . name } configured successfully.` } ) ;
195-
19694 return {
19795 kind : 'success' ,
19896 success : true ,
199- agentId : agent . id ,
200- configPath,
201- transport,
202- warnings,
203- nextSteps,
97+ agentId : result . agentId ,
98+ configPath : result . configPath ,
99+ transport : result . transport ,
100+ warnings : result . warnings ,
101+ nextSteps : [ ] ,
204102 } ;
205103 } catch ( err : unknown ) {
206104 return {
207105 kind : 'failure' ,
208106 success : false ,
209- warnings,
210- nextSteps,
107+ warnings : [ ] ,
108+ nextSteps : [ ] ,
211109 error : err instanceof Error ? err : new Error ( String ( err ) ) ,
212110 } ;
213111 }
214112}
215113
216114/**
217- * List every agent id known to the `setupMcp` function. Useful for
218- * consumer UIs that want to render a picker.
115+ * List every agent id known to `setupMcp`. Useful for consumer UIs that want to render a picker.
219116 */
220117export function listAgentIds ( ) : string [ ] {
221118 return getAgentIds ( ) ;
0 commit comments