Skip to content

Commit f8315a8

Browse files
jonathanMLDevzho
andauthored
Consolidate query and keyword_search response schemas (#180)
* Consolidate query and keyword_search response schemas via derived .partial() * update README.md for issue #174 --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 35d0441 commit f8315a8

8 files changed

Lines changed: 124 additions & 62 deletions

README.md

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![License: BSL-1.0](https://img.shields.io/badge/License-BSL--1.0-blue.svg)](https://opensource.org/licenses/BSL-1.0)
66
[![CI](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/workflows/CI/badge.svg)](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions)
77

8-
A Model Context Protocol (MCP) server that provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking.
8+
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that implements the MCP specification via `@modelcontextprotocol/sdk` v1.25+ and provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking.
99

1010
**Current version: 0.2.0** (npm `latest` after publish). Pin `@0.2.0` in install and MCP config for reproducible upgrades.
1111

@@ -19,18 +19,18 @@ While the package is **`0.y.z`**, minor releases may include breaking changes ([
1919

2020
## Documentation
2121

22-
| Doc | Description |
23-
| ---------------------------------------------- | -------------------------------------- |
24-
| [docs/README.md](docs/README.md) | Index of all guides |
25-
| [docs/TOOLS.md](docs/TOOLS.md) | Tool catalog & flows |
26-
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Env vars, CLI flags, library config |
27-
| [docs/FAQ.md](docs/FAQ.md) | Common questions |
28-
| [docs/MIGRATION.md](docs/MIGRATION.md) | Deprecations & breaking changes |
29-
| [docs/deprecation-policy.md](docs/deprecation-policy.md) | Release & deprecation policy |
30-
| [docs/CI_CD.md](docs/CI_CD.md) | GitHub Actions, SBOM, Docker, releases |
31-
| [docs/RELEASING.md](docs/RELEASING.md) | npm publish via GitHub Releases |
32-
| [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | How to contribute |
33-
| [docs/SECURITY.md](docs/SECURITY.md) | Vulnerability reporting |
22+
| Doc | Description |
23+
| -------------------------------------------------------- | -------------------------------------- |
24+
| [docs/README.md](docs/README.md) | Index of all guides |
25+
| [docs/TOOLS.md](docs/TOOLS.md) | Tool catalog & flows |
26+
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Env vars, CLI flags, library config |
27+
| [docs/FAQ.md](docs/FAQ.md) | Common questions |
28+
| [docs/MIGRATION.md](docs/MIGRATION.md) | Deprecations & breaking changes |
29+
| [docs/deprecation-policy.md](docs/deprecation-policy.md) | Release & deprecation policy |
30+
| [docs/CI_CD.md](docs/CI_CD.md) | GitHub Actions, SBOM, Docker, releases |
31+
| [docs/RELEASING.md](docs/RELEASING.md) | npm publish via GitHub Releases |
32+
| [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | How to contribute |
33+
| [docs/SECURITY.md](docs/SECURITY.md) | Vulnerability reporting |
3434

3535
## Error responses
3636

@@ -149,7 +149,11 @@ Module-level singleton facades (`setPineconeClient`, `registerUrlGenerator`, `ge
149149
```ts
150150
const config = resolveAllianceConfig({ apiKey: '...' });
151151
const ctx = createServer(config);
152-
ctx.setClient(new PineconeClient({ /* ... */ }));
152+
ctx.setClient(
153+
new PineconeClient({
154+
/* ... */
155+
})
156+
);
153157
const server = await setupAllianceServer({ context: ctx });
154158
```
155159

@@ -163,9 +167,16 @@ Process-default / facade-based setup remains available during the deprecation wi
163167

164168
```ts
165169
import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp';
166-
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
170+
import {
171+
resolveAllianceConfig,
172+
setupAllianceServer,
173+
} from '@will-cppa/pinecone-read-only-mcp/alliance';
167174

168-
setPineconeClient(new PineconeClient({ /* ... */ }));
175+
setPineconeClient(
176+
new PineconeClient({
177+
/* ... */
178+
})
179+
);
169180
const server = await setupAllianceServer(resolveAllianceConfig({ apiKey: '...' }));
170181
// Call teardownServer() before re-initializing the process-default context.
171182
```
@@ -185,7 +196,10 @@ import {
185196
type UrlGenerationResult,
186197
type UrlGeneratorFn,
187198
} from '@will-cppa/pinecone-read-only-mcp';
188-
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
199+
import {
200+
resolveAllianceConfig,
201+
setupAllianceServer,
202+
} from '@will-cppa/pinecone-read-only-mcp/alliance';
189203

190204
const config = resolveAllianceConfig({ apiKey: '...' }); // optional: indexName, rerankModel
191205
const ctx = createServer(config);
@@ -217,12 +231,12 @@ A fuller embedding sample lives in [examples/alliance/custom-url-generator.ts](e
217231

218232
**Alliance / advanced**[examples/alliance/](examples/alliance/):
219233

220-
| File | Description |
221-
| ---- | ----------- |
222-
| [examples/alliance/suggest-flow-demo.ts](examples/alliance/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
223-
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
224-
| [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) | **setupAllianceServer** without the CLI |
225-
| [examples/alliance/custom-url-generator.ts](examples/alliance/custom-url-generator.ts) | Custom **URL generator** registration |
234+
| File | Description |
235+
| ------------------------------------------------------------------------------------------ | -------------------------------------------------- |
236+
| [examples/alliance/suggest-flow-demo.ts](examples/alliance/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
237+
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
238+
| [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) | **setupAllianceServer** without the CLI |
239+
| [examples/alliance/custom-url-generator.ts](examples/alliance/custom-url-generator.ts) | Custom **URL generator** registration |
226240

227241
Run with `npx tsx examples/<path>.ts` from a checkout (requires valid Pinecone env for live paths). See [examples/README.md](examples/README.md).
228242

src/core/server/response-schemas.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,49 @@ describe('response-schemas', () => {
8585
expect(() => keywordSearchSuccessResponseSchema.parse({ status: 'success' })).toThrow();
8686
});
8787

88+
it('accepts every strict query payload in the permissive derived schema', () => {
89+
const strictPayload = {
90+
status: 'success' as const,
91+
mode: 'query_fast' as const,
92+
query: 'q',
93+
namespace: 'wg21',
94+
result_count: 1,
95+
results: [
96+
{
97+
document_id: 'D1',
98+
paper_number: 'D1',
99+
title: 'T',
100+
author: 'A',
101+
url: '',
102+
content: 'c',
103+
score: 0.9,
104+
reranked: false,
105+
},
106+
],
107+
};
108+
expect(querySuccessResponseSchema.parse(strictPayload)).toBeDefined();
109+
expect(queryResponseSchema.parse(strictPayload)).toBeDefined();
110+
});
111+
112+
it('accepts permissive-only keyword search payload but rejects strict validation', () => {
113+
const permissiveOnly = { status: 'success' as const };
114+
expect(keywordSearchResponseSchema.parse(permissiveOnly)).toBeDefined();
115+
expect(() => keywordSearchSuccessResponseSchema.parse(permissiveOnly)).toThrow();
116+
});
117+
118+
it('accepts every strict keyword search payload in the permissive derived schema', () => {
119+
const strictPayload = {
120+
status: 'success' as const,
121+
query: 'kw',
122+
namespace: 'wg21',
123+
index: 'test-index-sparse',
124+
result_count: 0,
125+
results: [] as [],
126+
};
127+
expect(keywordSearchSuccessResponseSchema.parse(strictPayload)).toBeDefined();
128+
expect(keywordSearchResponseSchema.parse(strictPayload)).toBeDefined();
129+
});
130+
88131
it('rejects query response missing status', () => {
89132
expect(() => queryResponseSchema.parse({ results: [] })).toThrow();
90133
});

src/core/server/response-schemas.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/**
22
* Zod schemas for MCP tool success responses.
33
* Types are derived via `z.infer` — single source of truth for response contracts.
4+
*
5+
* Single canonical schema per response type. Handler-boundary schemas define required
6+
* stable fields. Permissive exports are derived via `.partial()` for `guided_query` union
7+
* reuse and backward-compatible client validation.
48
*/
59

610
import { z } from 'zod';
@@ -70,21 +74,7 @@ export const guidedQueryExperimentalSchema = z.object({
7074
decision_trace: guidedQueryDecisionTraceSchema,
7175
});
7276

73-
export const queryResponseSchema = z.object({
74-
status: z.literal('success'),
75-
mode: z.enum(['query', 'query_fast', 'query_detailed']).optional(),
76-
query: z.string().optional(),
77-
namespace: z.string().optional(),
78-
metadata_filter: z.record(z.string(), z.unknown()).optional(),
79-
result_count: z.number().optional(),
80-
fields: z.array(z.string()).optional(),
81-
results: z.array(queryResultRowSchema).optional(),
82-
experimental: queryExperimentalSchema.optional(),
83-
});
84-
85-
export type QueryResponse = z.infer<typeof queryResponseSchema>;
86-
87-
/** Strict handler-boundary schema for `query` / `query_fast` / `query_detailed` success payloads. */
77+
/** Handler-boundary schema for `query` / `query_fast` / `query_detailed` success payloads. */
8878
export const querySuccessResponseSchema = z.object({
8979
status: z.literal('success'),
9080
mode: z.enum(['query', 'query_fast', 'query_detailed']),
@@ -99,6 +89,17 @@ export const querySuccessResponseSchema = z.object({
9989

10090
export type QuerySuccessResponse = z.infer<typeof querySuccessResponseSchema>;
10191

92+
/** Permissive query shape for `guided_query` union reuse and client-side validation. */
93+
export const queryResponseSchema = querySuccessResponseSchema.partial({
94+
mode: true,
95+
query: true,
96+
namespace: true,
97+
result_count: true,
98+
results: true,
99+
});
100+
101+
export type QueryResponse = z.infer<typeof queryResponseSchema>;
102+
102103
export const listNamespacesResponseSchema = z.object({
103104
status: z.literal('success'),
104105
cache_hit: z.boolean(),
@@ -148,21 +149,7 @@ export const countResponseSchema = z.object({
148149

149150
export type CountResponse = z.infer<typeof countResponseSchema>;
150151

151-
export const keywordSearchResponseSchema = z.object({
152-
status: z.literal('success'),
153-
query: z.string().optional(),
154-
namespace: z.string().optional(),
155-
index: z.string().optional(),
156-
metadata_filter: z.record(z.string(), z.unknown()).optional(),
157-
result_count: z.number().optional(),
158-
results: z.array(queryResultRowSchema).optional(),
159-
fields: z.array(z.string()).optional(),
160-
});
161-
162-
/** @deprecated Import from `response-schemas` / package root; alias kept for one minor cycle. */
163-
export type KeywordSearchResponse = z.infer<typeof keywordSearchResponseSchema>;
164-
165-
/** Strict handler-boundary schema for `keyword_search` success payloads. */
152+
/** Handler-boundary schema for `keyword_search` success payloads. */
166153
export const keywordSearchSuccessResponseSchema = z.object({
167154
status: z.literal('success'),
168155
query: z.string(),
@@ -176,6 +163,18 @@ export const keywordSearchSuccessResponseSchema = z.object({
176163

177164
export type KeywordSearchSuccessResponse = z.infer<typeof keywordSearchSuccessResponseSchema>;
178165

166+
/** Permissive keyword_search shape for client-side validation. */
167+
export const keywordSearchResponseSchema = keywordSearchSuccessResponseSchema.partial({
168+
query: true,
169+
namespace: true,
170+
index: true,
171+
result_count: true,
172+
results: true,
173+
});
174+
175+
/** @deprecated Import from `response-schemas` / package root; alias kept for one minor cycle. */
176+
export type KeywordSearchResponse = z.infer<typeof keywordSearchResponseSchema>;
177+
179178
const queryDocumentRowSchema = z.object({
180179
document_id: z.string(),
181180
merged_content: z.string(),

src/core/server/tools/keyword-search-tool.context.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, vi } from 'vitest';
22
import { registerKeywordSearchTool } from './keyword-search-tool.js';
3-
import { keywordSearchResponseSchema } from '../response-schemas.js';
3+
import { keywordSearchSuccessResponseSchema } from '../response-schemas.js';
44
import {
55
createMockServer,
66
createTestServerContext,
@@ -27,7 +27,7 @@ describe('keyword_search tool handler (ServerContext instance path)', () => {
2727
top_k: 5,
2828
});
2929
const body = parseToolJson(raw);
30-
expectMatchesResponseSchema(keywordSearchResponseSchema, body);
30+
expectMatchesResponseSchema(keywordSearchSuccessResponseSchema, body);
3131
expect(body).toMatchObject({
3232
status: 'success',
3333
query: 'contracts',

src/core/server/tools/keyword-search-tool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import {
1515
import {
1616
keywordSearchSuccessResponseSchema,
1717
type KeywordSearchResponse,
18+
type KeywordSearchSuccessResponse,
1819
} from '../response-schemas.js';
1920
import { jsonErrorResponse, validatedJsonResponse } from '../tool-response.js';
2021

2122
/** @deprecated Import {@link KeywordSearchResponse} from `response-schemas` or package root. */
2223
export type { KeywordSearchResponse };
2324

2425
type KeywordSearchExecResult =
25-
| { ok: true; body: KeywordSearchResponse }
26+
| { ok: true; body: KeywordSearchSuccessResponse }
2627
| { ok: false; error: ToolError };
2728

2829
async function executeKeywordSearch(
@@ -81,7 +82,7 @@ async function executeKeywordSearch(
8182
ctx,
8283
});
8384

84-
const response: KeywordSearchResponse = {
85+
const response: KeywordSearchSuccessResponse = {
8586
status: 'success',
8687
query: normalizedQuery,
8788
namespace: normalizedNamespace,

src/core/server/tools/query-tool.context.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, vi } from 'vitest';
22
import { registerQueryTool } from './query-tool.js';
3-
import { queryResponseSchema } from '../response-schemas.js';
3+
import { querySuccessResponseSchema } from '../response-schemas.js';
44
import {
55
assertToolErrorCode,
66
createMockServer,
@@ -30,7 +30,7 @@ describe('query tool handler (ServerContext instance path)', () => {
3030
preset: 'fast',
3131
});
3232
const body = parseToolJson(raw);
33-
expectMatchesResponseSchema(queryResponseSchema, body);
33+
expectMatchesResponseSchema(querySuccessResponseSchema, body);
3434
expect(body).toMatchObject({
3535
status: 'success',
3636
mode: 'query_fast',

src/core/server/tools/query-tool.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
import { z } from 'zod';
33
import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../../constants.js';
4-
import type { QueryResponse } from '../../../types.js';
54
import { getPineconeClient } from '../client-context.js';
65
import { formatQueryResultRows } from '../format-query-result.js';
76
import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js';
@@ -15,7 +14,11 @@ import {
1514
logToolError,
1615
validationToolError,
1716
} from '../tool-error.js';
18-
import { buildQueryExperimental, querySuccessResponseSchema } from '../response-schemas.js';
17+
import {
18+
buildQueryExperimental,
19+
querySuccessResponseSchema,
20+
type QuerySuccessResponse,
21+
} from '../response-schemas.js';
1922
import { jsonErrorResponse, validatedJsonResponse } from '../tool-response.js';
2023

2124
type QueryMode = 'query' | 'query_fast' | 'query_detailed';
@@ -76,7 +79,7 @@ async function executeQuery(params: QueryExecParams, ctx?: ServerContext) {
7679

7780
const formattedResults = formatQueryResultRows(queryOutcome.results, { ctx });
7881

79-
const response: QueryResponse = {
82+
const response: QuerySuccessResponse = {
8083
status: 'success',
8184
mode,
8285
query: query_text,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export type KeywordIndexNamespacesResult =
124124
export type {
125125
QueryResultRowShape,
126126
QueryResponse,
127+
QuerySuccessResponse,
127128
KeywordSearchResponse,
129+
KeywordSearchSuccessResponse,
128130
} from './core/server/response-schemas.js';
129131

130132
/** Internal merged hit shape before rerank (dense + sparse deduped). */

0 commit comments

Comments
 (0)