Skip to content

Commit c6be6b1

Browse files
jonathanMLDevzho
andauthored
feat: Zod-validated tool responses and stable/experimental field taxonomy (#151)
* feat: Zod-validated tool responses and stable/experimental field taxonomy Nest experimental MCP fields (degraded, decision_trace, etc.) under experimental, add response-schemas.ts for all 9 tools with runtime validation, and document the stable/experimental boundary. Fixes #139, #140 * addressed review agent results * addressed reviews --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 61605dd commit c6be6b1

39 files changed

Lines changed: 922 additions & 214 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release
1010

1111
### Changed
1212

13+
- **Breaking (MCP):** Experimental tool response fields are nested under `experimental` on success payloads. Affected tools: `query`, `query_documents`, `guided_query`. Fields moved: `degraded`, `degradation_reason`, `hybrid_leg_failed`, `rerank_skipped_reason` (query-shaped tools); `decision_trace` (`guided_query`). Stable fields (`status`, `results`, `namespace`, etc.) are unchanged. See [MIGRATION.md](docs/MIGRATION.md#unreleased-stable-vs-experimental-response-fields).
1314
- **Breaking (core):** `resolveConfig` requires a Pinecone index name and no longer applies Alliance index/rerank defaults. Removed exported `DEFAULT_INDEX_NAME` and `DEFAULT_RERANK_MODEL` from the package root. Rerank is opt-in when `PINECONE_RERANK_MODEL` / `rerankModel` is unset.
1415
- **Breaking (core):** `setupCoreServer` MCP `instructions` use `CORE_SERVER_INSTRUCTIONS` (no `guided_query` / `suggest_query_params`). `resolveConfig` defaults `disableSuggestFlow` to `true` so `query` / `count` / `query_documents` work without Alliance tools. Alliance CLI / `resolveAllianceConfig` unchanged: gate on by default, `ALLIANCE_SERVER_INSTRUCTIONS`.
1516
- **Alliance CLI / `resolveAllianceConfig`:** When index or rerank env/CLI values are omitted, defaults remain `rag-hybrid` and `bge-reranker-v2-m3` (API-key-only MCP configs unchanged). See [examples/alliance/.env.example](examples/alliance/.env.example).
1617
### Added
1718

19+
- Zod schemas for all nine MCP tool success responses (`queryResponseSchema`, `guidedQueryResponseSchema`, etc.) exported from the package root for client-side validation. Success payloads are runtime-validated before return.
20+
- Stable vs experimental response field taxonomy documented in [docs/TOOLS.md](docs/TOOLS.md) and [docs/deprecation-policy.md](docs/deprecation-policy.md#stable-vs-experimental-mcp-response-fields).
1821
- Formal deprecation policy ([docs/deprecation-policy.md](docs/deprecation-policy.md)) and breaking-change release notes template ([docs/templates/breaking-change-release-notes.md](docs/templates/breaking-change-release-notes.md)).
1922

2023
## [0.2.0] - 2026-05-29

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ To try the server on **your own** Pinecone project (free tier, no Alliance index
104104
The codebase is split into two layers:
105105

106106
- **`src/core/`** — generic MCP–Pinecone bridge (`PineconeClient`, `resolveConfig`, core MCP tools). Import from `@will-cppa/pinecone-read-only-mcp` (package root).
107-
- **`src/alliance/`** — C++ Alliance app tools (`suggest_query_params`, `guided_query`, Boost/Slack URL builtins). Import from `@will-cppa/pinecone-read-only-mcp/alliance`; embed via `resolveAllianceConfig``createServer` / `setClient``setupAllianceServer({ context: ctx })` (see [Library embedding](#library-embedding)).
107+
- **`src/alliance/`** — C++ Alliance app tools (`suggest_query_params`, `guided_query`, Boost/Slack URL builtins). Import from `@will-cppa/pinecone-read-only-mcp/alliance` and use `createServer(config)``ctx.setClient(...)``setupAllianceServer({ context: ctx })` for the full tool surface (what the CLI uses); see [Library embedding](#library-embedding) below.
108108

109109
## Configuration
110110

@@ -198,7 +198,7 @@ A fuller embedding sample lives in [examples/alliance/custom-url-generator.ts](e
198198
| File | Description |
199199
| ---- | ----------- |
200200
| [examples/alliance/suggest-flow-demo.ts](examples/alliance/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
201-
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `decision_trace` |
201+
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
202202
| [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) | **setupAllianceServer** without the CLI |
203203
| [examples/alliance/custom-url-generator.ts](examples/alliance/custom-url-generator.ts) | Custom **URL generator** registration |
204204

@@ -424,7 +424,7 @@ Single orchestrator tool that runs the full flow in one call:
424424
2. query param suggestion,
425425
3. execution via `count` or hybrid `query` (`fast` / `detailed` / `full` presets).
426426

427-
It returns both the final result and a `decision_trace` for transparency.
427+
It returns both the final result and `experimental.decision_trace` for transparency.
428428

429429
**Parameters:**
430430

@@ -437,7 +437,7 @@ It returns both the final result and a `decision_trace` for transparency.
437437
| `preferred_tool` | enum | No | `auto` | One of `auto`, `count`, `fast`, `detailed`, `full` |
438438
| `enrich_urls` | boolean | No | `true` | Auto-generate URLs for `mailing` and `slack-Cpplang` when `metadata.url` is missing |
439439

440-
**Returns:** JSON containing `decision_trace` and `result`.
440+
**Returns:** JSON containing `experimental.decision_trace` and `result`.
441441

442442
### `generate_urls`
443443

docs/CONFIGURATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Configuration is built from **CLI flags** (when using the binary), **environment
2828

2929
**Throws** if `apiKey` or `indexName` is missing after trim.
3030

31-
For the full Alliance tool surface (including `suggest_query_params`, `guided_query`, and built-in URL generators), import from `@will-cppa/pinecone-read-only-mcp/alliance` and follow the [Library embedding](#library-embedding) flow: `resolveAllianceConfig``createServer` / `setClient``setupAllianceServer({ context: ctx })`.
31+
For the full Alliance tool surface (including `suggest_query_params`, `guided_query`, and built-in URL generators), import from `@will-cppa/pinecone-read-only-mcp/alliance` and use the three-step instance-first recipe at [Library embedding](#library-embedding) below.
3232

3333
### Core vs Alliance resolvers
3434

docs/CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ Follow [deprecation-policy.md](./deprecation-policy.md). In summary:
4949
- **Breaking** a release (especially while `0.y.z`): use labeled bullets — `**Breaking (MCP):**`, `**Breaking (types):**`, etc. — each with what changed, who is affected, and a link to a MIGRATION anchor.
5050
- **Removing** deprecated APIs: add `### Removed` only after the deprecation window; keep migration text in MIGRATION.md.
5151

52+
## Response field stability
53+
54+
When changing MCP tool **success** response shapes:
55+
56+
- New fields default to the **`experimental`** nested object unless explicitly promoted (see [deprecation-policy.md § Stable vs experimental](./deprecation-policy.md#stable-vs-experimental-mcp-response-fields)).
57+
- Update Zod schemas in `src/core/server/response-schemas.ts` and export from `src/core/index.ts`.
58+
- Update [TOOLS.md](./TOOLS.md) stable/experimental tables for the affected tool(s).
59+
5260
## Documentation
5361

5462
Authoritative reference lives under [`docs/`](./README.md). When adding tools or config knobs, update `docs/TOOLS.md` and `docs/CONFIGURATION.md` in the same PR when possible.

docs/MIGRATION.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,69 @@ This guide is for **library and MCP client authors** upgrading from earlier **0.
66

77
Under [semver 0.y.z](https://semver.org/spec/v2.0.0.html#spec-item-4), **0.1.x → 0.2.0 is a breaking minor** — pin `@0.2.0` only after reading this guide.
88

9+
## Unreleased: Stable vs experimental response fields
10+
11+
**Rationale:** Tool success payloads mixed stable contract fields with experimental diagnostics (`degraded`, `decision_trace`, etc.) at the top level. Experimental fields are now nested under `experimental` so consumers know which fields are safe across minor version bumps.
12+
13+
**Who is affected:** MCP clients and integrators parsing `query`, `query_documents`, or `guided_query` success JSON.
14+
15+
**Before (`query`):**
16+
17+
```json
18+
{
19+
"status": "success",
20+
"results": [],
21+
"degraded": true,
22+
"degradation_reason": "rerank_failed: timeout",
23+
"hybrid_leg_failed": "dense"
24+
}
25+
```
26+
27+
**After (`query`):**
28+
29+
```json
30+
{
31+
"status": "success",
32+
"results": [],
33+
"experimental": {
34+
"degraded": true,
35+
"degradation_reason": "rerank_failed: timeout",
36+
"hybrid_leg_failed": "dense"
37+
}
38+
}
39+
```
40+
41+
**Before (`guided_query`):**
42+
43+
```json
44+
{
45+
"status": "success",
46+
"decision_trace": { "selected_namespace": "mailing" },
47+
"result": { "status": "success", "results": [], "degraded": false }
48+
}
49+
```
50+
51+
**After (`guided_query`):**
52+
53+
```json
54+
{
55+
"status": "success",
56+
"experimental": {
57+
"decision_trace": { "selected_namespace": "mailing", "rerank_status": "success" }
58+
},
59+
"result": {
60+
"status": "success",
61+
"results": []
62+
}
63+
}
64+
```
65+
66+
When no experimental fields apply, the `experimental` key is **omitted** (not an empty object).
67+
68+
**Client-side validation:** Import Zod schemas from the package root, e.g. `import { queryResponseSchema } from '@will-cppa/pinecone-read-only-mcp'`.
69+
70+
**Promotion:** Moving a field from `experimental` to stable requires CHANGELOG, TOOLS.md, and schema updates per [deprecation-policy.md § Stable vs experimental](./deprecation-policy.md#stable-vs-experimental-mcp-response-fields).
71+
972
## Unreleased: `ServerContext` instance APIs (phase 1)
1073

1174
**Rationale:** Process-global singletons (Pinecone client slot, config, URL registry, suggest-flow gate, namespaces cache) complicate testing and multi-tenant embedding. Phase 1 introduces an opt-in **`ServerContext`** without removing legacy getters.

docs/TOOLS.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
Unless noted, failures return MCP `isError: true` with JSON matching `ToolError` (see [MIGRATION.md](./MIGRATION.md) and [README error table](../README.md#error-responses)).
44

5+
## Response field stability
6+
7+
Success payloads separate **stable** fields (safe across minor bumps after `1.0.0`) from **experimental** fields (may change before `1.0.0`). Experimental fields are nested under `experimental` when present; the key is omitted when empty.
8+
9+
| Tool | Stable | Experimental |
10+
| ---- | ------ | ------------ |
11+
| `list_namespaces` | `status`, `cache_hit`, `cache_ttl_seconds`, `expires_at_iso`, `count`, `namespaces` | _(none)_ |
12+
| `namespace_router` | `status`, `cache_hit`, `user_query`, `suggestions`, `recommended_namespace` | _(none)_ |
13+
| `suggest_query_params` | `status`, `cache_hit`, `suggested_fields`, `recommended_tool`, `use_count_tool`, `explanation`, `namespace_found` | _(none)_ |
14+
| `count` | `status`, `count`, `truncated`, `namespace`, `metadata_filter` | _(none)_ |
15+
| `query` | `status`, `mode`, `query`, `namespace`, `metadata_filter`, `result_count`, `results`, `fields` | `experimental.degraded`, `experimental.degradation_reason`, `experimental.hybrid_leg_failed`, `experimental.rerank_skipped_reason` |
16+
| `keyword_search` | `status`, `query`, `namespace`, `index`, `metadata_filter`, `result_count`, `results`, `fields` | _(none)_ |
17+
| `query_documents` | `status`, `query`, `namespace`, `metadata_filter`, `result_count`, `documents` | Same experimental degradation fields as `query` |
18+
| `guided_query` | `status`, `result` (count or query-shaped stable fields) | `experimental.decision_trace`; query-path `result.experimental` degradation fields |
19+
| `generate_urls` | `status`, `namespace`, `count`, `results` | _(none)_ |
20+
21+
Promotion process: [deprecation-policy.md § Stable vs experimental](./deprecation-policy.md#stable-vs-experimental-mcp-response-fields).
22+
523
## Core vs Alliance tool surface
624

725
| Setup | Tools | MCP instructions |
@@ -108,7 +126,7 @@ When **`disableSuggestFlow`** is **`true`** (core default via `resolveConfig`),
108126
| `metadata_filter` | object | no | Metadata filter |
109127
| `fields` | string[] | no | Pinecone fields to return |
110128

111-
**Success (`QueryResponse`):** `{ status: 'success', mode?: 'query' \| 'query_fast' \| 'query_detailed', query, namespace, metadata_filter?, result_count, results[], fields?, degraded?, degradation_reason?, hybrid_leg_failed? }`.
129+
**Success (`QueryResponse`):** `{ status: 'success', mode?, query, namespace, metadata_filter?, result_count, results[], fields?, experimental?: { degraded?, degradation_reason?, hybrid_leg_failed?, rerank_skipped_reason? } }`.
112130

113131
Each row: `document_id`, `paper_number` (deprecated alias), `title`, `author`, `url`, `content`, `score`, `reranked`, optional `metadata`.
114132

@@ -125,17 +143,17 @@ Each row: `document_id`, `paper_number` (deprecated alias), `title`, `author`, `
125143

126144
### Rerank and hybrid degradation
127145

128-
When reranking is requested but the rerank API fails, the server still returns **`status: 'success'`** with rows where `reranked: false`, plus envelope fields:
146+
When reranking is requested but the rerank API fails, the server still returns **`status: 'success'`** with rows where `reranked: false`, plus **experimental** envelope fields:
129147

130148
| Field | When set | Meaning |
131149
| ----- | -------- | ------- |
132-
| `degraded` | `true` | Rerank was attempted and failed (or another degradation path fired) |
133-
| `degradation_reason` | string | Human-readable detail for MCP/LLM clients (e.g. `rerank_failed: timeout after 5000ms`) |
134-
| `hybrid_leg_failed` | `'dense'` \| `'sparse'` \| omitted / `null` | Exactly one hybrid search leg failed while the other returned hits |
150+
| `experimental.degraded` | `true` | Rerank was attempted and failed (or another degradation path fired) |
151+
| `experimental.degradation_reason` | string | Human-readable detail for MCP/LLM clients (e.g. `rerank_failed: timeout after 5000ms`) |
152+
| `experimental.hybrid_leg_failed` | `'dense'` \| `'sparse'` | Exactly one hybrid search leg failed while the other returned hits |
135153

136-
Treat **`degraded: true`** as lower confidence even when `status` is `success`. Combine with per-row `reranked`, `preset`, and `use_reranking`. Structured stderr logs may include additional detail.
154+
Treat **`experimental.degraded: true`** as lower confidence even when `status` is `success`. Combine with per-row `reranked`, `preset`, and `use_reranking`. Structured stderr logs may include additional detail.
137155

138-
`query_documents` propagates the same flags on its nested query payload when applicable.
156+
`query_documents` propagates the same experimental flags when applicable.
139157

140158
---
141159

@@ -167,7 +185,7 @@ Treat **`degraded: true`** as lower confidence even when `status` is `success`.
167185
| `metadata_filter` | object | no | Filter |
168186
| `max_chunks_per_document` | int | no | Cap merged chunks per doc (default 200, max 500) |
169187

170-
**Success:** `{ status: 'success', query, namespace, metadata_filter?, result_count, documents: [{ document_id, merged_content, metadata, chunk_count, best_score }] }`.
188+
**Success:** `{ status: 'success', query, namespace, metadata_filter?, result_count, documents[], experimental?: { degraded?, degradation_reason?, hybrid_leg_failed?, rerank_skipped_reason? } }`.
171189

172190
---
173191

@@ -184,11 +202,11 @@ Treat **`degraded: true`** as lower confidence even when `status` is `success`.
184202
| `preferred_tool` | `auto` \| `count` \| `fast` \| `detailed` \| `full` | no | Override automated tool choice |
185203
| `enrich_urls` | boolean | no (default true) | Run URL generator when metadata lacks `url` |
186204

187-
**Success:** `{ status: 'success', decision_trace, result }` where `result` is either a count payload or a `QueryResponse`-shaped query payload.
205+
**Success:** `{ status: 'success', experimental: { decision_trace }, result }` where `result` is either a count payload or a `QueryResponse`-shaped query payload.
188206

189-
**`decision_trace` fields (non-exhaustive):** `cache_hit`, `input_namespace`, `routed_namespace`, `selected_namespace`, `ranked_namespaces`, `suggested_fields`, `suggested_tool`, `selected_tool`, `explanation`, `enrich_urls`, `rerank_status` (`success` \| `skipped` \| `failed`).
207+
**`experimental.decision_trace` fields (non-exhaustive):** `cache_hit`, `input_namespace`, `routed_namespace`, `selected_namespace`, `ranked_namespaces`, `suggested_fields`, `suggested_tool`, `selected_tool`, `explanation`, `enrich_urls`, `rerank_status` (`success` \| `skipped` \| `skipped_no_model` \| `failed`).
190208

191-
When the inner query path runs, `result` includes the same `degraded`, `degradation_reason`, and `hybrid_leg_failed` fields as `query` (see [Rerank and hybrid degradation](#rerank-and-hybrid-degradation)).
209+
When the inner query path runs, `result.experimental` includes the same degradation fields as `query` (see [Rerank and hybrid degradation](#rerank-and-hybrid-degradation)).
192210

193211
**Example:**
194212

@@ -227,4 +245,4 @@ Single-shot:
227245
guided_query
228246
```
229247

230-
Canonical Zod schemas live beside each handler under `src/server/tools/*.ts`.
248+
Canonical Zod input schemas live beside each handler under `src/core/server/tools/*.ts` and `src/alliance/tools/*.ts`. Success response schemas are in `src/core/server/response-schemas.ts` and exported from the package root (e.g. `queryResponseSchema`, `guidedQueryResponseSchema`).

docs/deprecation-policy.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,33 @@ While **`0.y.z`**, a minor release **may** ship breaking changes without a prior
6464

6565
Security fixes may break behavior when required; document impact in CHANGELOG and MIGRATION.
6666

67+
## Stable vs experimental MCP response fields
68+
69+
MCP tool **success** responses separate fields into two tiers:
70+
71+
| Tier | Meaning | Location in JSON |
72+
| ---- | ------- | ---------------- |
73+
| **Stable** | Safe to depend on across minor version bumps after `1.0.0` | Top-level keys (`status`, `results`, `namespace`, `count`, etc.) |
74+
| **Experimental** | May change or be removed before `1.0.0` | Nested under `experimental` when present |
75+
76+
Current experimental fields (see [TOOLS.md](./TOOLS.md) per tool):
77+
78+
- `degraded`, `degradation_reason`, `hybrid_leg_failed`, `rerank_skipped_reason` — query-shaped tools (`query`, `query_documents`, and `guided_query.result` when applicable)
79+
- `decision_trace``guided_query` only
80+
81+
The `experimental` key is **omitted** when no experimental fields are set.
82+
83+
### Promoting experimental → stable
84+
85+
To promote a field from experimental to stable:
86+
87+
1. **CHANGELOG**`### Changed` entry describing the promotion and target release.
88+
2. **TOOLS.md** — Move the field from the Experimental to Stable table for the affected tool(s).
89+
3. **Schema** — Move the field out of `experimental` in `response-schemas.ts` and update handler builders.
90+
4. **MIGRATION.md** — Document before/after for integrators.
91+
92+
New response fields added before `1.0.0` default to `experimental` unless explicitly promoted through this process.
93+
6794
## Future instance APIs (`ServerContext`)
6895

6996
Phase 1 of the **`ServerContext`** / **`createServer(config)`** refactor is available while legacy module-level getters remain supported. See [MIGRATION.md § ServerContext instance APIs](./MIGRATION.md#unreleased-servercontext-instance-apis-phase-1) for upgrade steps.

examples/alliance/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
These examples target the **full** MCP surface via `setupAllianceServer` from `@will-cppa/pinecone-read-only-mcp/alliance`:
44

55
- `suggest_query_params` and the suggest-flow gate
6-
- `guided_query` with `decision_trace`
6+
- `guided_query` with `experimental.decision_trace`
77
- Built-in URL generators for `mailing` and `slack-Cpplang`
88

99
They assume a Pinecone index you control with compatible data (not necessarily the C++ Alliance production index). To bootstrap a **neutral** index from scratch, start with [examples/quickstart/README.md](../quickstart/README.md).
@@ -24,7 +24,7 @@ Optional: `PINECONE_RERANK_MODEL`, `PINECONE_SPARSE_INDEX_NAME`, etc. See [docs/
2424
| File | Description |
2525
| ---- | ----------- |
2626
| [suggest-flow-demo.ts](./suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
27-
| [guided-query-demo.ts](./guided-query-demo.ts) | **guided_query** and `decision_trace` |
27+
| [guided-query-demo.ts](./guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
2828
| [library-embedding-demo.ts](./library-embedding-demo.ts) | Programmatic **setupAllianceServer** wiring |
2929
| [custom-url-generator.ts](./custom-url-generator.ts) | Custom **URL generator** registration |
3030
| [demo-mock-pinecone-client.ts](./demo-mock-pinecone-client.ts) | Mock client with `mailing` namespace (no network) |

0 commit comments

Comments
 (0)