mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
76c8c349b1
* refactor(server): fold @trek/nest-mcp into server/src/nest-mcp * docs(server): point nest-mcp references at server/src/nest-mcp * remove tracked md file
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import { McpController, McpModule, McpRegistryService, Tool, type McpAccessPolicy } from '../../../src/nest-mcp';
|
|
import { createAttachHarness, type AttachHarness, type TestCtx } from './harness';
|
|
import { Injectable, Module } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
|
|
import 'reflect-metadata';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { z } from 'zod';
|
|
|
|
@Injectable()
|
|
class CounterService {
|
|
private n = 0;
|
|
next(): number {
|
|
return ++this.n;
|
|
}
|
|
}
|
|
|
|
@McpController()
|
|
class CounterMcp {
|
|
constructor(private readonly counter: CounterService) {}
|
|
|
|
@Tool({ name: 'count', inputSchema: { by: z.number().optional() }, access: { group: 'trips', mode: 'read' } })
|
|
async count(_args: { by?: number }, ctx: TestCtx) {
|
|
return { content: [{ type: 'text', text: JSON.stringify({ value: this.counter.next(), userId: ctx.userId }) }] };
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
class PlainProvider {
|
|
ignoredMethod() {}
|
|
}
|
|
|
|
@Module({ providers: [CounterService, CounterMcp], exports: [] })
|
|
class CounterModule {}
|
|
|
|
const policy: McpAccessPolicy = (_access, ctx) => (ctx as TestCtx).canRead === true;
|
|
|
|
describe('McpRegistryService discovery', () => {
|
|
let harness: AttachHarness | undefined;
|
|
afterEach(async () => {
|
|
await harness?.cleanup();
|
|
harness = undefined;
|
|
});
|
|
|
|
it('discovers only @McpController providers, across feature modules', async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [McpModule.forRoot({ accessPolicy: policy }), CounterModule],
|
|
providers: [PlainProvider],
|
|
}).compile();
|
|
await moduleRef.init();
|
|
|
|
const registry = moduleRef.get(McpRegistryService);
|
|
expect(registry.list()).toEqual([
|
|
expect.objectContaining({ kind: 'tool', name: 'count', className: 'CounterMcp', methodName: 'count' }),
|
|
]);
|
|
await moduleRef.close();
|
|
});
|
|
|
|
it('registers boot-discovered entries with working constructor injection', async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [McpModule.forRoot({ accessPolicy: policy }), CounterModule],
|
|
}).compile();
|
|
await moduleRef.init();
|
|
|
|
const registry = moduleRef.get(McpRegistryService);
|
|
harness = await createAttachHarness(registry, { userId: 5, canRead: true });
|
|
const result = await harness.client.callTool({ name: 'count', arguments: {} });
|
|
const text = (result.content as { type: string; text: string }[]).find((c) => c.type === 'text');
|
|
expect(JSON.parse(text?.text ?? '')).toEqual({ value: 1, userId: 5 });
|
|
await moduleRef.close();
|
|
});
|
|
|
|
it('fails app boot (onModuleInit) on duplicate tool names', async () => {
|
|
@McpController()
|
|
class CountClone {
|
|
@Tool({ name: 'count' })
|
|
async countAgain() {
|
|
return { content: [{ type: 'text', text: 'clone' }] };
|
|
}
|
|
}
|
|
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [McpModule.forRoot({ accessPolicy: policy }), CounterModule],
|
|
providers: [CountClone],
|
|
}).compile();
|
|
await expect(moduleRef.init()).rejects.toThrow(/duplicate MCP registrations: tool "count"/);
|
|
});
|
|
|
|
it('boots without an accessPolicy and still attaches predicate/ungated entries', async () => {
|
|
@McpController()
|
|
class OpenMcp {
|
|
@Tool({ name: 'open' })
|
|
async open() {
|
|
return { content: [{ type: 'text', text: 'ok' }] };
|
|
}
|
|
}
|
|
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [McpModule.forRoot()],
|
|
providers: [OpenMcp],
|
|
}).compile();
|
|
await moduleRef.init();
|
|
|
|
const registry = moduleRef.get(McpRegistryService);
|
|
harness = await createAttachHarness(registry, {});
|
|
expect((await harness.client.listTools()).tools.map((t) => t.name)).toEqual(['open']);
|
|
await moduleRef.close();
|
|
});
|
|
});
|