Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions packages/domscribe-relay/src/cli/commands/mcp.command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from 'commander';
import { RelayControl } from '../../lifecycle/relay-control.js';
import { createMcpAdapter } from '../../mcp/mcp-adapter.js';
import { createMcpAdapter, McpAdapter } from '../../mcp/mcp-adapter.js';
import { getWorkspaceRoot } from '../utils.js';

interface McpCommandOptions {
Expand All @@ -24,16 +24,39 @@ export const McpCommand = new Command('mcp')
}
});

function setupShutdownHandlers(adapter: McpAdapter): void {
const shutdown = async (signal: string): Promise<void> => {
console.error(`\n[domscribe-cli] Received ${signal}, shutting down MCP...`);
await adapter.close();
process.exit(0);
};

process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}

async function mcp(options: McpCommandOptions) {
const { debug } = options;
const workspaceRoot = getWorkspaceRoot();

if (!workspaceRoot) {
throw new Error(
'No workspace root found. Ensure you are running this command inside a workspace where domscribe is installed.',
);
if (debug) {
console.error(
'[domscribe-cli] No workspace found, starting in dormant mode',
);
}

const adapter = createMcpAdapter({
mode: 'dormant',
cwd: process.cwd(),
debug,
});

await adapter.start();
setupShutdownHandlers(adapter);
return;
}

const { debug } = options;
const bodyLimit = options.bodyLimit
? parseInt(options.bodyLimit, 10)
: undefined;
Expand All @@ -47,22 +70,13 @@ async function mcp(options: McpCommandOptions) {
`[domscribe-cli] Starting MCP adapter (relay at http://${relayHost}:${relayPort})`,
);

// Create and start MCP adapter
const adapter = createMcpAdapter({
mode: 'active',
relayHost,
relayPort,
debug,
});

await adapter.start();

// Handle shutdown
const shutdown = async (signal: string): Promise<void> => {
console.error(`\n[domscribe-cli] Received ${signal}, shutting down MCP...`);
await adapter.close();
process.exit(0);
};

process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
setupShutdownHandlers(adapter);
}
194 changes: 131 additions & 63 deletions packages/domscribe-relay/src/mcp/mcp-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,91 +37,159 @@ vi.mock('../client/relay-http-client.js', () => ({
},
}));

function getServer(adapter: McpAdapter) {
return (
adapter as unknown as {
server: {
registeredTools: Map<string, unknown>;
registeredPrompts: Map<string, unknown>;
};
}
).server;
}

describe('McpAdapter', () => {
it('should register all 12 tools', () => {
// Act
const adapter = new McpAdapter({
relayHost: 'localhost',
relayPort: 9876,
describe('active mode', () => {
it('should register all 12 tools', () => {
// Act
const adapter = new McpAdapter({
mode: 'active',
relayHost: 'localhost',
relayPort: 9876,
});

// Assert
const server = getServer(adapter);
expect(server.registeredTools.size).toBe(12);
expect(server.registeredTools.has('domscribe.resolve')).toBe(true);
expect(server.registeredTools.has('domscribe.resolve.batch')).toBe(true);
expect(server.registeredTools.has('domscribe.manifest.stats')).toBe(true);
expect(server.registeredTools.has('domscribe.manifest.query')).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.get')).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.list')).toBe(
true,
);
expect(server.registeredTools.has('domscribe.annotation.process')).toBe(
true,
);
expect(
server.registeredTools.has('domscribe.annotation.updateStatus'),
).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.respond')).toBe(
true,
);
expect(server.registeredTools.has('domscribe.annotation.search')).toBe(
true,
);
expect(server.registeredTools.has('domscribe.status')).toBe(true);
expect(server.registeredTools.has('domscribe.query.bySource')).toBe(true);
});

// Assert — access internal server to verify registration
const server = (
adapter as unknown as {
server: { registeredTools: Map<string, unknown> };
}
).server;
expect(server.registeredTools.size).toBe(12);
expect(server.registeredTools.has('domscribe.resolve')).toBe(true);
expect(server.registeredTools.has('domscribe.resolve.batch')).toBe(true);
expect(server.registeredTools.has('domscribe.manifest.stats')).toBe(true);
expect(server.registeredTools.has('domscribe.manifest.query')).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.get')).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.list')).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.process')).toBe(
true,
);
expect(
server.registeredTools.has('domscribe.annotation.updateStatus'),
).toBe(true);
expect(server.registeredTools.has('domscribe.annotation.respond')).toBe(
true,
);
expect(server.registeredTools.has('domscribe.annotation.search')).toBe(
true,
);
expect(server.registeredTools.has('domscribe.status')).toBe(true);
expect(server.registeredTools.has('domscribe.query.bySource')).toBe(true);
});
it('should register all 4 prompts', () => {
// Act
const adapter = new McpAdapter({
mode: 'active',
relayHost: 'localhost',
relayPort: 9876,
});

// Assert
const server = getServer(adapter);
expect(server.registeredPrompts.size).toBe(4);
expect(server.registeredPrompts.has('process_next')).toBe(true);
expect(server.registeredPrompts.has('check_status')).toBe(true);
expect(server.registeredPrompts.has('explore_component')).toBe(true);
expect(server.registeredPrompts.has('find_annotations')).toBe(true);
});

it('should register all 4 prompts', () => {
// Act
const adapter = new McpAdapter({
relayHost: 'localhost',
relayPort: 9876,
it('should start and connect transport', async () => {
const adapter = new McpAdapter({
mode: 'active',
relayHost: 'localhost',
relayPort: 9876,
});

// Should not throw
await adapter.start();
});

// Assert
const server = (
adapter as unknown as {
server: { registeredPrompts: Map<string, unknown> };
}
).server;
expect(server.registeredPrompts.size).toBe(4);
expect(server.registeredPrompts.has('process_next')).toBe(true);
expect(server.registeredPrompts.has('check_status')).toBe(true);
expect(server.registeredPrompts.has('explore_component')).toBe(true);
expect(server.registeredPrompts.has('find_annotations')).toBe(true);
it('should close gracefully', async () => {
const adapter = new McpAdapter({
mode: 'active',
relayHost: 'localhost',
relayPort: 9876,
});

// Should not throw
await adapter.close();
});
});

it('should start and connect transport', async () => {
const adapter = new McpAdapter({
relayHost: 'localhost',
relayPort: 9876,
describe('dormant mode', () => {
it('should register only the status tool', () => {
// Act
const adapter = new McpAdapter({
mode: 'dormant',
cwd: '/home/user/some-project',
});

// Assert
const server = getServer(adapter);
expect(server.registeredTools.size).toBe(1);
expect(server.registeredTools.has('domscribe.status')).toBe(true);
});

// Should not throw
await adapter.start();
});
it('should register no prompts', () => {
// Act
const adapter = new McpAdapter({
mode: 'dormant',
cwd: '/home/user/some-project',
});

it('should close gracefully', async () => {
const adapter = new McpAdapter({
relayHost: 'localhost',
relayPort: 9876,
// Assert
const server = getServer(adapter);
expect(server.registeredPrompts.size).toBe(0);
});

// Should not throw
await adapter.close();
it('should start and connect transport', async () => {
const adapter = new McpAdapter({
mode: 'dormant',
cwd: '/home/user/some-project',
});

// Should not throw
await adapter.start();
});

it('should close gracefully', async () => {
const adapter = new McpAdapter({
mode: 'dormant',
cwd: '/home/user/some-project',
});

// Should not throw
await adapter.close();
});
});
});

describe('createMcpAdapter', () => {
it('should return an McpAdapter instance', () => {
it('should return an McpAdapter instance for active mode', () => {
const adapter = createMcpAdapter({
mode: 'active',
relayHost: 'localhost',
relayPort: 9876,
});

expect(adapter).toBeInstanceOf(McpAdapter);
});

it('should return an McpAdapter instance for dormant mode', () => {
const adapter = createMcpAdapter({
mode: 'dormant',
cwd: '/tmp/test',
});

expect(adapter).toBeInstanceOf(McpAdapter);
});
});
Loading
Loading