Skip to content

Commit 09efc3c

Browse files
jonathanMLDevzho
andauthored
Completes the context-complete tool path (#142)
* Completes the context-complete tool path * addressed format check * addressed ai reviews * addressed reviewer's opinion- remove setDefaultServerContext(null) --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 687a9a6 commit 09efc3c

17 files changed

Lines changed: 648 additions & 26 deletions

src/alliance/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function setupAllianceServer(config?: ServerConfig): Promise<McpSer
1919
instructions: ALLIANCE_SERVER_INSTRUCTIONS,
2020
});
2121
const ctx = getDefaultServerContext();
22-
registerBuiltinUrlGenerators();
22+
registerBuiltinUrlGenerators(ctx);
2323
registerSuggestQueryParamsTool(server, ctx);
2424
registerGuidedQueryTool(server, ctx);
2525
return server;

src/alliance/tools/guided-query-tool.context.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
import { describe, expect, it, vi } from 'vitest';
2+
import { registerBuiltinUrlGenerators } from '../url-builtins.js';
3+
import { registerQueryTool } from '../../core/server/tools/query-tool.js';
4+
import { registerSuggestQueryParamsTool } from './suggest-query-params-tool.js';
25
import { registerGuidedQueryTool } from './guided-query-tool.js';
36
import {
7+
assertToolErrorCode,
48
createMockServer,
59
createTestServerContext,
610
makeHybridQueryResult,
11+
makeSearchResult,
712
parseToolJson,
813
} from '../../core/server/tools/test-helpers.js';
914

15+
const namespaceMetadata = {
16+
document_number: 'string',
17+
title: 'string',
18+
url: 'string',
19+
author: 'string',
20+
chunk_text: 'string',
21+
};
22+
23+
function papersNamespaceClient(overrides?: { query?: ReturnType<typeof vi.fn> }) {
24+
return {
25+
listNamespacesWithMetadata: vi
26+
.fn()
27+
.mockResolvedValue([{ namespace: 'papers', recordCount: 42, metadata: namespaceMetadata }]),
28+
query: overrides?.query ?? vi.fn().mockResolvedValue(makeHybridQueryResult()),
29+
count: vi.fn().mockResolvedValue({ count: 7, truncated: false }),
30+
};
31+
}
32+
1033
describe('guided_query tool handler (ServerContext instance path)', () => {
1134
it('returns success with decision_trace using injected context', async () => {
1235
const listNamespacesWithMetadata = vi.fn().mockResolvedValue([
@@ -53,4 +76,137 @@ describe('guided_query tool handler (ServerContext instance path)', () => {
5376
expect(trace['rerank_status']).toBeDefined();
5477
expect(query).toHaveBeenCalledOnce();
5578
});
79+
80+
it('surfaces degraded and hybrid_leg_failed in result', async () => {
81+
const query = vi.fn().mockResolvedValue(
82+
makeHybridQueryResult({
83+
degraded: true,
84+
degradation_reason: 'sparse_leg_empty',
85+
hybrid_leg_failed: 'sparse',
86+
})
87+
);
88+
const ctx = createTestServerContext({
89+
client: papersNamespaceClient({ query }) as never,
90+
});
91+
const server = createMockServer();
92+
registerGuidedQueryTool(server as never, ctx);
93+
const body = parseToolJson(
94+
await server.getHandler('guided_query')!({
95+
user_query: 'contracts',
96+
namespace: 'papers',
97+
preferred_tool: 'fast',
98+
enrich_urls: false,
99+
})
100+
);
101+
const result = body['result'] as Record<string, unknown>;
102+
expect(result['degraded']).toBe(true);
103+
expect(result['hybrid_leg_failed']).toBe('sparse');
104+
expect(result['degradation_reason']).toBe('sparse_leg_empty');
105+
});
106+
107+
it('enriches urls via ctx builtins when enrich_urls is true', async () => {
108+
const mailingDocId = 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6';
109+
const query = vi.fn().mockResolvedValue(
110+
makeHybridQueryResult({
111+
results: [
112+
makeSearchResult({
113+
metadata: {
114+
document_number: 'MSG-1',
115+
title: 'T',
116+
author: 'A',
117+
doc_id: mailingDocId,
118+
},
119+
}),
120+
],
121+
})
122+
);
123+
const ctx = createTestServerContext({
124+
client: {
125+
listNamespacesWithMetadata: vi.fn().mockResolvedValue([
126+
{
127+
namespace: 'mailing',
128+
recordCount: 42,
129+
metadata: {
130+
document_number: 'string',
131+
title: 'string',
132+
author: 'string',
133+
chunk_text: 'string',
134+
},
135+
},
136+
]),
137+
query,
138+
count: vi.fn(),
139+
} as never,
140+
});
141+
registerBuiltinUrlGenerators(ctx);
142+
const server = createMockServer();
143+
registerGuidedQueryTool(server as never, ctx);
144+
const body = parseToolJson(
145+
await server.getHandler('guided_query')!({
146+
user_query: 'announcement',
147+
namespace: 'mailing',
148+
preferred_tool: 'fast',
149+
enrich_urls: true,
150+
})
151+
);
152+
const result = body['result'] as Record<string, unknown>;
153+
const rows = result['results'] as Array<{ url: string }>;
154+
expect(rows[0]?.url).toContain('lists.boost.org');
155+
});
156+
157+
it('returns TIMEOUT when orchestrator client throws timeout error', async () => {
158+
const query = vi
159+
.fn()
160+
.mockRejectedValue(new Error('Timeout after 5000ms while waiting for query'));
161+
const ctx = createTestServerContext({
162+
client: papersNamespaceClient({ query }) as never,
163+
});
164+
const server = createMockServer();
165+
registerGuidedQueryTool(server as never, ctx);
166+
const err = assertToolErrorCode(
167+
await server.getHandler('guided_query')!({
168+
user_query: 'contracts',
169+
namespace: 'papers',
170+
preferred_tool: 'fast',
171+
enrich_urls: false,
172+
}),
173+
'TIMEOUT'
174+
);
175+
expect(err.suggestion).toMatch(/retry|timeout/i);
176+
});
177+
178+
it('does not block explicit suggest_query_params after internal suggest', async () => {
179+
const ctx = createTestServerContext({
180+
client: papersNamespaceClient() as never,
181+
});
182+
const guidedServer = createMockServer();
183+
registerGuidedQueryTool(guidedServer as never, ctx);
184+
await guidedServer.getHandler('guided_query')!({
185+
user_query: 'What does the paper say?',
186+
namespace: 'papers',
187+
preferred_tool: 'fast',
188+
enrich_urls: false,
189+
});
190+
191+
const suggestServer = createMockServer();
192+
registerSuggestQueryParamsTool(suggestServer as never, ctx);
193+
const suggestBody = parseToolJson(
194+
await suggestServer.getHandler('suggest_query_params')!({
195+
namespace: 'papers',
196+
user_query: 'List titles',
197+
})
198+
);
199+
expect(suggestBody['status']).toBe('success');
200+
201+
const queryServer = createMockServer();
202+
registerQueryTool(queryServer as never, ctx);
203+
const queryBody = parseToolJson(
204+
await queryServer.getHandler('query')!({
205+
query_text: 'List titles',
206+
namespace: 'papers',
207+
preset: 'fast',
208+
})
209+
);
210+
expect(queryBody['status']).toBe('success');
211+
});
56212
});

src/alliance/tools/guided-query-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export function registerGuidedQueryTool(server: McpServer, ctx?: ServerContext):
229229
const formattedResults = formatQueryResultRows(queryOutcome.results, {
230230
namespace,
231231
enrichUrls: enrich_urls,
232+
ctx,
232233
});
233234
const result: QueryResponse = {
234235
status: 'success',
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { registerBuiltinUrlGenerators } from '../url-builtins.js';
3+
import { registerGuidedQueryTool } from './guided-query-tool.js';
4+
import {
5+
createMockServer,
6+
createTestServerContext,
7+
isolateFromDefaultContext,
8+
makeHybridQueryResult,
9+
makeSearchResult,
10+
parseToolJson,
11+
} from '../../core/server/tools/test-helpers.js';
12+
import {
13+
getDefaultServerContext,
14+
teardownDefaultServerContext,
15+
} from '../../core/server/server-context.js';
16+
17+
const MAILING_DOC_ID = 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6';
18+
const EXPECTED_MAILING_URL = `https://lists.boost.org/archives/list/${MAILING_DOC_ID}/`;
19+
20+
describe('isolated ServerContext with zero default context', () => {
21+
beforeEach(() => {
22+
isolateFromDefaultContext();
23+
});
24+
25+
afterEach(() => {
26+
teardownDefaultServerContext();
27+
});
28+
29+
it('guided_query enrich_urls uses ctx builtins, not default registry', async () => {
30+
const listNamespacesWithMetadata = vi.fn().mockResolvedValue([
31+
{
32+
namespace: 'mailing',
33+
recordCount: 42,
34+
metadata: {
35+
document_number: 'string',
36+
title: 'string',
37+
author: 'string',
38+
chunk_text: 'string',
39+
},
40+
},
41+
]);
42+
const query = vi.fn().mockResolvedValue(
43+
makeHybridQueryResult({
44+
results: [
45+
makeSearchResult({
46+
metadata: {
47+
document_number: 'MSG-1',
48+
title: 'T',
49+
author: 'A',
50+
doc_id: MAILING_DOC_ID,
51+
},
52+
}),
53+
],
54+
})
55+
);
56+
const ctx = createTestServerContext({
57+
client: {
58+
listNamespacesWithMetadata,
59+
query,
60+
count: vi.fn(),
61+
} as never,
62+
});
63+
registerBuiltinUrlGenerators(ctx);
64+
65+
const server = createMockServer();
66+
registerGuidedQueryTool(server as never, ctx);
67+
const raw = await server.getHandler('guided_query')!({
68+
user_query: 'What was announced?',
69+
namespace: 'mailing',
70+
preferred_tool: 'fast',
71+
enrich_urls: true,
72+
});
73+
const body = parseToolJson(raw);
74+
const result = body['result'] as Record<string, unknown>;
75+
const rows = result['results'] as Array<{ url: string }>;
76+
expect(rows[0]?.url).toBe(EXPECTED_MAILING_URL);
77+
expect(getDefaultServerContext().hasUrlGenerator('mailing')).toBe(false);
78+
});
79+
});

src/alliance/tools/suggest-query-params-tool.context.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
1-
import { describe, expect, it, vi } from 'vitest';
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import { registerQueryTool } from '../../core/server/tools/query-tool.js';
23
import { registerSuggestQueryParamsTool } from './suggest-query-params-tool.js';
34
import {
45
createMockServer,
56
createTestServerContext,
7+
makeHybridQueryResult,
68
parseToolJson,
79
} from '../../core/server/tools/test-helpers.js';
810

11+
const namespaceMetadata = {
12+
document_number: 'string',
13+
title: 'string',
14+
url: 'string',
15+
author: 'string',
16+
chunk_text: 'string',
17+
};
18+
19+
function mockNamespacesClient() {
20+
return {
21+
listNamespacesWithMetadata: vi
22+
.fn()
23+
.mockResolvedValue([{ namespace: 'wg21', recordCount: 42, metadata: namespaceMetadata }]),
24+
};
25+
}
26+
927
describe('suggest_query_params tool handler (ServerContext instance path)', () => {
28+
afterEach(() => {
29+
vi.useRealTimers();
30+
});
31+
1032
it('marks suggest-flow on injected context when namespace exists', async () => {
1133
const listNamespacesWithMetadata = vi.fn().mockResolvedValue([
1234
{
@@ -41,4 +63,55 @@ describe('suggest_query_params tool handler (ServerContext instance path)', () =
4163
const flowCheck = ctx.requireSuggested('wg21');
4264
expect(flowCheck.ok).toBe(true);
4365
});
66+
67+
it('updates suggestion state when called twice for the same namespace', async () => {
68+
const ctx = createTestServerContext({
69+
client: mockNamespacesClient() as never,
70+
});
71+
const server = createMockServer();
72+
registerSuggestQueryParamsTool(server as never, ctx);
73+
const handler = server.getHandler('suggest_query_params')!;
74+
75+
await handler({ namespace: 'wg21', user_query: 'List papers with titles' });
76+
await handler({ namespace: 'wg21', user_query: 'how many records match?' });
77+
78+
const flowCheck = ctx.requireSuggested('wg21');
79+
expect(flowCheck.ok).toBe(true);
80+
if (flowCheck.ok) {
81+
expect(flowCheck.flow.user_query).toBe('how many records match?');
82+
expect(flowCheck.flow.recommended_tool).toBe('count');
83+
}
84+
});
85+
86+
it('re-suggest after expiry allows query without FLOW_GATE', async () => {
87+
vi.useFakeTimers();
88+
const query = vi.fn().mockResolvedValue(makeHybridQueryResult());
89+
const ctx = createTestServerContext({
90+
config: { cacheTtlSeconds: 1 },
91+
client: { ...mockNamespacesClient(), query } as never,
92+
});
93+
const suggestServer = createMockServer();
94+
registerSuggestQueryParamsTool(suggestServer as never, ctx);
95+
const suggestHandler = suggestServer.getHandler('suggest_query_params')!;
96+
97+
await suggestHandler({ namespace: 'wg21', user_query: 'contracts' });
98+
vi.advanceTimersByTime(2000);
99+
100+
const reSuggestBody = parseToolJson(
101+
await suggestHandler({ namespace: 'wg21', user_query: 'contracts again' })
102+
);
103+
expect(reSuggestBody['status']).toBe('success');
104+
105+
const queryServer = createMockServer();
106+
registerQueryTool(queryServer as never, ctx);
107+
const queryBody = parseToolJson(
108+
await queryServer.getHandler('query')!({
109+
query_text: 'contracts again',
110+
namespace: 'wg21',
111+
preset: 'fast',
112+
})
113+
);
114+
expect(queryBody['status']).toBe('success');
115+
expect(query).toHaveBeenCalledOnce();
116+
});
44117
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { registerBuiltinUrlGenerators } from './url-builtins.js';
3+
import { createTestServerContext } from '../core/server/tools/test-helpers.js';
4+
5+
const MAILING_DOC_ID = 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6';
6+
7+
describe('registerBuiltinUrlGenerators (ServerContext instance path)', () => {
8+
it('registers builtins only on the target context', () => {
9+
const ctxA = createTestServerContext();
10+
const ctxB = createTestServerContext();
11+
registerBuiltinUrlGenerators(ctxA);
12+
13+
const metadata = { doc_id: MAILING_DOC_ID };
14+
const fromA = ctxA.generateUrlForNamespace('mailing', metadata);
15+
const fromB = ctxB.generateUrlForNamespace('mailing', metadata);
16+
17+
expect(fromA.url).toContain('lists.boost.org');
18+
expect(fromA.method).toBe('generated.mailing');
19+
expect(fromB.method).toBe('unavailable');
20+
expect(fromB.url).toBeNull();
21+
});
22+
});

0 commit comments

Comments
 (0)