mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(mcp): add tool safety annotations * test(mcp): address PR #1127 review follow-ups - Replace private `_requestHandlers` SDK access in server.test.ts with `Client` + `InMemoryTransport.createLinkedPair()` for the tools/list annotation propagation test. The new path uses supported public APIs and surfaces SDK changes loudly instead of silently degrading. - Extract `OPEN_WORLD_READ_ONLY_TOOLS` set in tools.test.ts so future read-only open-world tools can be added without rewriting the invariant; preserves the current "only `query` is open-world" guard. - Add inline rationale on `group_sync` annotations explaining the conservative `idempotentHint: false` (writes contracts.json on every call even when output is deterministic). No runtime behavior change. Annotations themselves and tools/list shape are unchanged. --------- Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
127 lines
5.3 KiB
TypeScript
127 lines
5.3 KiB
TypeScript
/**
|
|
* Unit Tests: MCP Server
|
|
*
|
|
* Tests: createMCPServer from server.ts
|
|
* - Server creation returns a Server instance
|
|
* - Tool handler wraps backend.callTool and appends hints
|
|
* - Tool handler catches errors and returns isError: true
|
|
* - Resource handlers delegate to resources.ts functions
|
|
* - Prompt handlers return expected prompts
|
|
* - Next-step hints cover all tool names
|
|
*
|
|
* NOTE: We test the server handler logic by calling the request handlers
|
|
* directly through the MCP Server's handler dispatch.
|
|
*/
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
import { createMCPServer } from '../../src/mcp/server.js';
|
|
import { GITNEXUS_TOOLS } from '../../src/mcp/tools.js';
|
|
|
|
// ─── Mock backend ──────────────────────────────────────────────────
|
|
|
|
function createMockBackend(overrides: Record<string, any> = {}): any {
|
|
return {
|
|
callTool: vi.fn().mockResolvedValue({ result: 'ok' }),
|
|
listRepos: vi.fn().mockResolvedValue([]),
|
|
resolveRepo: vi
|
|
.fn()
|
|
.mockResolvedValue({ name: 'test', repoPath: '/tmp/test', lastCommit: 'abc' }),
|
|
getContext: vi.fn().mockReturnValue(null),
|
|
queryClusters: vi.fn().mockResolvedValue({ clusters: [] }),
|
|
queryProcesses: vi.fn().mockResolvedValue({ processes: [] }),
|
|
queryClusterDetail: vi.fn().mockResolvedValue({ error: 'not found' }),
|
|
queryProcessDetail: vi.fn().mockResolvedValue({ error: 'not found' }),
|
|
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ─── createMCPServer ─────────────────────────────────────────────────
|
|
|
|
describe('createMCPServer', () => {
|
|
it('returns a Server instance with expected shape', () => {
|
|
const backend = createMockBackend();
|
|
const server = createMCPServer(backend);
|
|
expect(server).toBeDefined();
|
|
// Server should have connect/close methods
|
|
expect(typeof server.connect).toBe('function');
|
|
expect(typeof server.close).toBe('function');
|
|
});
|
|
|
|
it('server has setRequestHandler method', () => {
|
|
const backend = createMockBackend();
|
|
const server = createMCPServer(backend);
|
|
// The server has registered handlers — verify it was created without errors
|
|
expect(server).toBeTruthy();
|
|
});
|
|
|
|
it('tools/list response includes tool annotations', async () => {
|
|
const backend = createMockBackend();
|
|
const server = createMCPServer(backend);
|
|
const client = new Client({ name: 'test-client', version: '0.0.0' });
|
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
|
|
try {
|
|
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
|
|
const response = await client.listTools();
|
|
expect(response.tools).toHaveLength(GITNEXUS_TOOLS.length);
|
|
|
|
for (const tool of response.tools) {
|
|
const definition = GITNEXUS_TOOLS.find((t) => t.name === tool.name)!;
|
|
expect(tool.annotations).toEqual(definition.annotations);
|
|
}
|
|
} finally {
|
|
await client.close();
|
|
await server.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── getNextStepHint (tested indirectly via server tool handler) ──────
|
|
|
|
describe('getNextStepHint (via tool call response)', () => {
|
|
// We test hints by calling the server's tool handler indirectly.
|
|
// Since createMCPServer registers handlers on the Server, we verify
|
|
// hints are appended by checking the tool response format.
|
|
|
|
it('query tool response includes hint about context', async () => {
|
|
const backend = createMockBackend({
|
|
callTool: vi.fn().mockResolvedValue({ processes: [], definitions: [] }),
|
|
});
|
|
const server = createMCPServer(backend);
|
|
|
|
// We can't easily call handlers directly on the MCP Server,
|
|
// so we verify the handler was registered by creating the server without error.
|
|
// The actual hint logic is tested via the integration path.
|
|
expect(backend.callTool).not.toHaveBeenCalled(); // not called until request
|
|
});
|
|
});
|
|
|
|
// ─── Tool handler error handling ──────────────────────────────────────
|
|
|
|
describe('server error handling', () => {
|
|
it('createMCPServer does not throw for valid backend', () => {
|
|
const backend = createMockBackend();
|
|
expect(() => createMCPServer(backend)).not.toThrow();
|
|
});
|
|
|
|
it('createMCPServer reads version from package.json', () => {
|
|
const backend = createMockBackend();
|
|
const server = createMCPServer(backend);
|
|
// Server was created with version from package.json — no crash
|
|
expect(server).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ─── Prompt definitions ───────────────────────────────────────────────
|
|
|
|
describe('prompt registration', () => {
|
|
it('server registers detect_impact and generate_map prompts', () => {
|
|
const backend = createMockBackend();
|
|
// Creating the server registers all handlers including prompts
|
|
const server = createMCPServer(backend);
|
|
expect(server).toBeDefined();
|
|
});
|
|
});
|