Skip to content

Commit e704246

Browse files
authored
feat: sdk-commands mcp params for unity explorer (#1496)
1 parent f928629 commit e704246

4 files changed

Lines changed: 199 additions & 4 deletions

File tree

packages/@dcl/sdk-commands/package-lock.json

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@dcl/sdk-commands/src/commands/start/explorer-alpha.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ import { CliComponents } from '../../components'
44

55
const isWindows = /^win/.test(process.platform)
66

7+
/**
8+
* Parses raw CLI tokens (everything after a standalone `--`) into deep link query params.
9+
* Supports `--key=value`, `--key value` and bare `--key` (mapped to `key=true`).
10+
* Tokens that are not flags and not consumed as a value are ignored.
11+
*/
12+
export function parsePassthroughParams(tokens: string[]): Map<string, string> {
13+
const params = new Map<string, string>()
14+
for (let i = 0; i < tokens.length; i++) {
15+
const token = tokens[i]
16+
if (!token.startsWith('-')) continue
17+
const stripped = token.replace(/^-+/, '')
18+
if (!stripped) continue
19+
const eqIndex = stripped.indexOf('=')
20+
if (eqIndex > 0) {
21+
params.set(stripped.slice(0, eqIndex), stripped.slice(eqIndex + 1))
22+
} else if (eqIndex === -1 && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
23+
params.set(stripped, tokens[i + 1])
24+
i++
25+
} else if (eqIndex === -1) {
26+
params.set(stripped, 'true')
27+
}
28+
}
29+
return params
30+
}
31+
732
export async function runExplorerAlpha(
833
components: CliComponents,
934
opts: {
@@ -47,6 +72,8 @@ async function runApp(
4772
const landscapeTerrainEnabled = !!args['--landscape-terrain-enabled']
4873
const openDeeplinkInNewInstance = !!args['-n']
4974
const multiInstance = !!args['--multi-instance']
75+
const mcp = !!args['--mcp']
76+
const mcpPort = args['--mcp-port']
5077

5178
try {
5279
if (isWindows) {
@@ -78,6 +105,20 @@ async function runApp(
78105
if (multiInstance) {
79106
params.set('multi-instance', 'true')
80107
}
108+
if (mcp) {
109+
params.set('mcp', 'true')
110+
}
111+
if (mcpPort !== undefined) {
112+
params.set('mcp-port', String(mcpPort))
113+
}
114+
115+
// Forward any params placed after a standalone `--` verbatim into the deep link.
116+
// Only fill in params that aren't already covered by a declared flag/default, so
117+
// passthrough can't silently override an intentionally declared flag.
118+
for (const [key, value] of parsePassthroughParams(args._ ?? [])) {
119+
if (params.has(key)) continue
120+
params.set(key, value)
121+
}
81122

82123
const queryParams = params.toString()
83124

packages/@dcl/sdk-commands/src/commands/start/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ export const args = declareArgs({
6868
'-n': Boolean,
6969
'--bevy-web': Boolean,
7070
'--multi-instance': Boolean,
71-
'--no-client': Boolean
71+
'--no-client': Boolean,
72+
'--mcp': Boolean,
73+
'--mcp-port': Number
7274
})
7375

7476
export async function help(options: Options) {
@@ -98,6 +100,15 @@ export async function help(options: Options) {
98100
--mobile Show QR code for mobile preview on the same network.
99101
--multi-instance Allow running multiple Explorer instances simultaneously.
100102
--no-client Suppress every auto-launch (desktop Explorer deeplink, browser open, mobile QR). The file watcher still notifies a desktop Explorer if it connects on its own — useful when an external tool owns the Explorer process.
103+
--mcp Enable the MCP server in the Explorer (forwarded as a deep link parameter).
104+
--mcp-port Port for the MCP server in the Explorer (forwarded as a deep link parameter).
105+
106+
Any argument placed after a standalone \`--\` is not parsed by the CLI and is forwarded verbatim
107+
into the Explorer deep link as a query parameter. Supported forms: --key=value, --key value,
108+
and bare --key (forwarded as key=true). Declared flags above take precedence over forwarded params.
109+
110+
$ sdk-commands start -- --paramA --paramX valueX
111+
$ npm run start -- -- --paramA --paramX valueX (npm consumes the first --)
101112
102113
103114
Examples:

test/sdk-commands/commands/start/explorer-alpha.spec.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,152 @@ describe('explorer-alpha', () => {
185185
})
186186
})
187187

188+
describe('mcp parameter', () => {
189+
it('should include mcp parameter when --mcp flag is provided', async () => {
190+
const args: any = {
191+
'--mcp': true
192+
}
193+
194+
await runExplorerAlpha(mockComponents, {
195+
cwd: '/test',
196+
realm: 'test-realm',
197+
baseCoords: { x: 0, y: 0 },
198+
isHub: false,
199+
args
200+
})
201+
202+
expect(mockExec).toHaveBeenCalledWith(
203+
'/test',
204+
'open',
205+
expect.arrayContaining([expect.stringContaining('mcp=true')]),
206+
{ silent: true }
207+
)
208+
})
209+
210+
it('should include mcp-port parameter when --mcp-port is provided', async () => {
211+
const args: any = {
212+
'--mcp': true,
213+
'--mcp-port': 8025
214+
}
215+
216+
await runExplorerAlpha(mockComponents, {
217+
cwd: '/test',
218+
realm: 'test-realm',
219+
baseCoords: { x: 0, y: 0 },
220+
isHub: false,
221+
args
222+
})
223+
224+
expect(mockExec).toHaveBeenCalledWith(
225+
'/test',
226+
'open',
227+
expect.arrayContaining([expect.stringContaining('mcp-port=8025')]),
228+
{ silent: true }
229+
)
230+
})
231+
232+
it('should not include mcp-port parameter when --mcp-port is not provided', async () => {
233+
const args: any = {}
234+
235+
await runExplorerAlpha(mockComponents, {
236+
cwd: '/test',
237+
realm: 'test-realm',
238+
baseCoords: { x: 0, y: 0 },
239+
isHub: false,
240+
args
241+
})
242+
243+
expect(mockExec).toHaveBeenCalledWith(
244+
'/test',
245+
'open',
246+
expect.arrayContaining([expect.not.stringContaining('mcp-port')]),
247+
{ silent: true }
248+
)
249+
})
250+
251+
it('should not include mcp parameter when --mcp flag is not provided', async () => {
252+
const args: any = {}
253+
254+
await runExplorerAlpha(mockComponents, {
255+
cwd: '/test',
256+
realm: 'test-realm',
257+
baseCoords: { x: 0, y: 0 },
258+
isHub: false,
259+
args
260+
})
261+
262+
expect(mockExec).toHaveBeenCalledWith(
263+
'/test',
264+
'open',
265+
expect.arrayContaining([expect.not.stringContaining('mcp=true')]),
266+
{ silent: true }
267+
)
268+
})
269+
})
270+
271+
describe('passthrough parameters (after standalone --)', () => {
272+
async function run(args: any) {
273+
await runExplorerAlpha(mockComponents, {
274+
cwd: '/test',
275+
realm: 'test-realm',
276+
baseCoords: { x: 0, y: 0 },
277+
isHub: false,
278+
args
279+
})
280+
return mockExec.mock.calls[0][2][0] as string
281+
}
282+
283+
it('forwards a bare flag as key=true', async () => {
284+
const deepLink = await run({ _: ['--paramA'] })
285+
expect(deepLink).toContain('paramA=true')
286+
})
287+
288+
it('forwards a flag followed by a value as key=value', async () => {
289+
const deepLink = await run({ _: ['--paramX', 'valueX'] })
290+
expect(deepLink).toContain('paramX=valueX')
291+
})
292+
293+
it('forwards --key=value syntax', async () => {
294+
const deepLink = await run({ _: ['--paramX=valueX'] })
295+
expect(deepLink).toContain('paramX=valueX')
296+
})
297+
298+
it('forwards multiple params, mixing bare flags and valued flags', async () => {
299+
const deepLink = await run({ _: ['--paramA', '--paramB', '--paramX', 'valueX'] })
300+
expect(deepLink).toContain('paramA=true')
301+
expect(deepLink).toContain('paramB=true')
302+
expect(deepLink).toContain('paramX=valueX')
303+
})
304+
305+
it('does not override built-in deep link params already covered by a declared flag/default', async () => {
306+
const deepLink = await run({ _: ['--realm', 'whatever.dcl.eth'] })
307+
expect(deepLink).toContain('realm=test-realm')
308+
expect(deepLink).not.toContain('realm=whatever.dcl.eth')
309+
})
310+
311+
it('does not override an explicitly declared flag', async () => {
312+
const deepLink = await run({ '--realm': 'declared.dcl.eth', _: ['--realm', 'whatever.dcl.eth'] })
313+
expect(deepLink).toContain('realm=declared.dcl.eth')
314+
expect(deepLink).not.toContain('realm=whatever.dcl.eth')
315+
})
316+
317+
it('fills in a param that is not covered by any declared flag/default', async () => {
318+
const deepLink = await run({ _: ['--paramA'] })
319+
expect(deepLink).toContain('paramA=true')
320+
})
321+
322+
it('ignores bare tokens that are not flags and not values of a flag', async () => {
323+
const deepLink = await run({ _: ['stray-positional', '--paramA'] })
324+
expect(deepLink).not.toContain('stray-positional')
325+
expect(deepLink).toContain('paramA=true')
326+
})
327+
328+
it('url-encodes forwarded values', async () => {
329+
const deepLink = await run({ _: ['--paramX', 'a value&other'] })
330+
expect(deepLink).toContain('paramX=a+value%26other')
331+
})
332+
})
333+
188334
describe('URL parameter construction', () => {
189335
it('should construct URL with all parameters correctly', async () => {
190336
const args: any = {

0 commit comments

Comments
 (0)