diff --git a/packages/core/src/node/__tests__/server-mcp.test.ts b/packages/core/src/node/__tests__/server-mcp.test.ts new file mode 100644 index 00000000..a1e8441a --- /dev/null +++ b/packages/core/src/node/__tests__/server-mcp.test.ts @@ -0,0 +1,63 @@ +import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { normalizeDevToolsConfig } from '../config' +import { setResolvedDevToolsConfig } from '../resolved-config' +import { createDevToolsHub } from '../server' + +const initHub = vi.hoisted(() => vi.fn()) + +vi.mock('@devframes/hub/initiate', () => ({ + initHub, +})) + +vi.mock('@devframes/json-render-ui/hub', () => ({ + jsonRenderUiRenderer: () => ({ type: 'json-render', file: '/builtin-json-render.mjs' }), +})) + +vi.mock('../ui', () => ({ + createViteDevToolsUi: () => ({}), +})) + +vi.mock('../auth-handler', () => ({ + getAuthHandler: () => ({ rpcFunctions: [] }), + isClientAuthDisabled: () => false, + isBuildCapabilityAuth: () => false, + getBuildCapabilityToken: () => 'test-capability-token', +})) + +function fakeContext(mcp?: boolean): ViteDevToolsNodeContext { + const context = { + mode: 'dev', + viteConfig: { devtools: undefined }, + host: { provideConnectionMeta: vi.fn() }, + } as unknown as ViteDevToolsNodeContext + setResolvedDevToolsConfig( + context, + normalizeDevToolsConfig(mcp === undefined ? true : { mcp }, 'localhost'), + ) + return context +} + +describe('createDevToolsHub mcp forwarding', () => { + beforeEach(() => { + vi.clearAllMocks() + initHub.mockReturnValue({ + ready: Promise.resolve(), + connectionMeta: () => ({}), + nodeMiddleware: vi.fn(), + close: vi.fn(), + }) + }) + + it('forwards an explicit `mcp: false` opt-out to initHub', async () => { + await createDevToolsHub({ context: fakeContext(false) }) + + expect(initHub.mock.calls[0]![0].mcp).toBe(false) + }) + + it('leaves `mcp` undeclared by default so the hub keeps its auto-mount', async () => { + await createDevToolsHub({ context: fakeContext() }) + + expect(initHub.mock.calls[0]![0]).not.toHaveProperty('mcp') + }) +}) diff --git a/packages/core/src/node/config.ts b/packages/core/src/node/config.ts index 5a844dae..6deb033d 100644 --- a/packages/core/src/node/config.ts +++ b/packages/core/src/node/config.ts @@ -1,3 +1,4 @@ +import type { McpSetting } from 'devframe/types' import type { StartOptions } from './cli-commands' import type { DevToolsUserOptions } from './plugin-options' @@ -53,6 +54,16 @@ export interface DevToolsConfig extends Partial, DevToolsUserOptio * hostnames here explicitly. */ allowedOrigins?: string[] + /** + * Expose the aggregate MCP endpoint at `__mcp`, a Streamable-HTTP + * server over the whole tool registry of every mounted devframe. + * + * Defaults to `'auto'`: the route mounts once any agent-flagged surface + * exists. Pass `false` to keep it off (e.g. when MCP is bridged elsewhere), + * `true` to mount it unconditionally behind the loopback origin gate, or an + * object to opt into an identity check. + */ + mcp?: McpSetting } export interface ResolvedDevToolsConfig { diff --git a/packages/core/src/node/server.ts b/packages/core/src/node/server.ts index 133353b6..7bec4707 100644 --- a/packages/core/src/node/server.ts +++ b/packages/core/src/node/server.ts @@ -70,7 +70,9 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom ? getBuildCapabilityToken(context) : undefined - const allowedOrigins = getResolvedDevToolsConfig(context).config.allowedOrigins + const resolvedConfig = getResolvedDevToolsConfig(context).config + const allowedOrigins = resolvedConfig.allowedOrigins + const mcp = resolvedConfig.mcp const hub = initHub({ base: DEVTOOLS_MOUNT_PATH, @@ -91,6 +93,7 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom ...(context.viteServer ? { clientModuleResolution: '/@id/{specifier}' } : {}), auth: authDisabled ? false : getAuthHandler(context), ...(allowedOrigins ? { allowedOrigins } : {}), + ...(mcp !== undefined ? { mcp } : {}), ...(options.server ? { server: options.server } : { ws: options.wsPort != null ? { port: options.wsPort } : { sidecar: true } }),