Skip to content

Commit 56ffc29

Browse files
jonathanMLDevzho
andauthored
Document teardown, degradation signals, and example mocks (#103)
* updated CHANGELOG.md * updated readme.md adding a missing word, added missing files * addressed ai review --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 00edba7 commit 56ffc29

7 files changed

Lines changed: 184 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release
2525
- Vitest **global** coverage thresholds in `vitest.config.ts` (lines 73%, statements 72%, branches 58%, functions 76% — measured baseline minus slack); `npm run test:coverage` exits non-zero when any bucket regresses.
2626
- `@vitest/coverage-v8` devDependency for coverage reports (`lcov`, `json-summary`, HTML).
2727
- `docs/` reference set (TOOLS, CONFIGURATION, SECURITY, CONTRIBUTING, CI_CD, FAQ, MIGRATION, RELEASING) and worked examples `examples/suggest-flow-demo.ts`, `examples/guided-query-demo.ts`, `examples/library-embedding-demo.ts`.
28+
- `teardownServer()` export to reset process-global MCP state (suggest-flow gate, namespaces cache, URL generator registry, active config, shared `PineconeClient`) so `setupServer()` can run again in the same Node process (tests, re-embedding).
29+
- Namespace trimming for the suggest-flow gate and gated tools (`normalizeNamespace`); use the same trimmed `namespace` for `suggest_query_params` and downstream `query` / `count` / `query_documents`.
30+
- Successful `query` / `query_documents` / `guided_query` payloads may include `degraded`, `degradation_reason`, and `hybrid_leg_failed` when rerank or a hybrid leg fails but the tool still returns hits; `guided_query` `decision_trace` adds `rerank_status`.
2831

2932
### Changed
3033

@@ -34,7 +37,8 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release
3437
- **Breaking (MCP):** Single hybrid `query` tool with `preset` (`fast` | `detailed` | `full`); removed separate `query_fast` / `query_detailed` tool registrations.
3538
- `resolveConfig()` throws if the Pinecone API key is missing (after trim); library callers must supply `apiKey` via overrides or set `PINECONE_API_KEY`.
3639
- `withTimeout` aborts an internal `AbortSignal` on deadline (cooperative cancellation).
37-
- `PineconeClient`: shared hit-field extraction, safer merge dedup without empty `_id` collisions, metadata sampling skips zero-vector probe when dimension is unknown, `listNamespacesFromKeywordIndex` surfaces errors via `{ ok: false }`.
40+
- `PineconeClient`: constructor reads index name, rerank model, and default top-k only from `PineconeClientConfig` (not `process.env`); shared hit-field extraction, safer merge dedup without empty `_id` collisions, metadata sampling skips zero-vector probe when dimension is unknown, `listNamespacesFromKeywordIndex` surfaces errors via `{ ok: false }`.
41+
- `setupServer()` throws if called twice in one process without `teardownServer()` first; README library-embedding section documents the teardown pattern.
3842
- Metadata filter manual validation accepts primitive arrays for `$in`/`$nin` including numbers (matches Zod).
3943
- README: deployment model for process-global gate/cache/registry; adjusted feature wording vs pre-1.0 semver.
4044
- `.npmignore` no longer excludes `dist/` (still shipped via `package.json` `files`).

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ When a tool fails, the MCP tool result sets **`isError: true`**. The `text` cont
3535

3636
Success payloads are unchanged and do **not** wrap `ToolError`. Clients that still expect `{ "status": "error", "message": "..." }` must migrate to the shape above.
3737

38-
For successful `query` / `guided_query` payloads, **rerank/hybrid fidelity** is described in [docs/TOOLS.md](docs/TOOLS.md) (row-level `reranked`, current lack of a top-level `degraded` envelope).
38+
For successful `query`, `query_documents`, and `guided_query` payloads, **rerank/hybrid fidelity** is described in [docs/TOOLS.md](docs/TOOLS.md#rerank-and-hybrid-degradation) (row-level `reranked`, top-level `degraded` / `degradation_reason`, and optional `hybrid_leg_failed`; `query_documents` propagates the same fields on its nested query payload when applicable).
3939

4040
## Features
4141

@@ -106,9 +106,11 @@ The server uses **process-global** memory for the suggest-flow gate (`suggest_qu
106106

107107
### Library embedding (`setupServer`)
108108

109-
Treat **`setupServer()` as one logical server per Node process**: it mutates shared module singletons (suggest-flow map, namespaces cache, URL registry, config context, shared `PineconeClient` slot). A second `setupServer()` without a coordinated teardown can leave stale or mixed state for in-flight requests — **spawn a separate process** per isolated instance until an explicit lifecycle API is documented in the changelog.
109+
Treat **`setupServer()` as one logical server per Node process**: it mutates shared module singletons (suggest-flow map, namespaces cache, URL registry, config context, shared `PineconeClient` slot). A **second** `setupServer()` in the same process **throws** unless you call **`teardownServer()`** first.
110110

111-
Recommended pattern: `resolveConfig``setPineconeClient(new PineconeClient(...))``await setupServer(config)` → connect one MCP transport. See [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate).
111+
Recommended pattern: `resolveConfig``setPineconeClient(new PineconeClient(...))``await setupServer(config)` → connect one MCP transport. For tests or re-initialization in the same process, call `teardownServer()` then `setupServer(config)` again. For isolated production tenants, prefer **one server per Node process** (or separate OS processes) rather than sharing one embedder across tenants.
112+
113+
Import `setupServer` and `teardownServer` from `@will-cppa/pinecone-read-only-mcp`. See [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate).
112114

113115
### Custom URL generators
114116

docs/TOOLS.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Tools **`query`**, **`count`**, and **`query_documents`** require a prior succes
9999
| `metadata_filter` | object | no | Metadata filter |
100100
| `fields` | string[] | no | Pinecone fields to return |
101101

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

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

@@ -114,9 +114,19 @@ Each row: `document_id`, `paper_number` (deprecated alias), `title`, `author`, `
114114
}
115115
```
116116

117-
### Rerank fallback and row-level fidelity
117+
### Rerank and hybrid degradation
118118

119-
When reranking is requested but the rerank API fails, the server still returns **`status: 'success'`** with rows where `reranked: false`. Treat **`reranked: false`** as lower confidence when reranking was expected (`preset` detailed/full). Structured stderr logs include the failure; there is **no** separate top-level `degraded` flag in the current JSON envelope—client UX should combine `preset`, `use_reranking`, and per-row `reranked` (see project issue backlog for envelope-level degradation).
119+
When reranking is requested but the rerank API fails, the server still returns **`status: 'success'`** with rows where `reranked: false`, plus envelope fields:
120+
121+
| Field | When set | Meaning |
122+
| ----- | -------- | ------- |
123+
| `degraded` | `true` | Rerank was attempted and failed (or another degradation path fired) |
124+
| `degradation_reason` | string | Human-readable detail for MCP/LLM clients (e.g. `rerank_failed: timeout after 5000ms`) |
125+
| `hybrid_leg_failed` | `'dense'` \| `'sparse'` \| omitted / `null` | Exactly one hybrid search leg failed while the other returned hits |
126+
127+
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.
128+
129+
`query_documents` propagates the same flags on its nested query payload when applicable.
120130

121131
---
122132

@@ -167,7 +177,9 @@ When reranking is requested but the rerank API fails, the server still returns *
167177

168178
**Success:** `{ status: 'success', decision_trace, result }` where `result` is either a count payload or a `QueryResponse`-shaped query payload.
169179

170-
**`decision_trace` fields (non-exhaustive):** `cache_hit`, `input_namespace`, `routed_namespace`, `selected_namespace`, `ranked_namespaces`, `suggested_fields`, `suggested_tool`, `selected_tool`, `explanation`, `enrich_urls`.
180+
**`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`).
181+
182+
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)).
171183

172184
**Example:**
173185

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Mock PineconeClient for examples: no network; returns canned namespaces and hits.
3+
* Namespace `mailing` matches built-in URL generator demos in the README.
4+
*/
5+
6+
import {
7+
PineconeClient,
8+
type CountParams,
9+
type CountResult,
10+
type HybridQueryResult,
11+
type KeywordIndexNamespacesResult,
12+
type KeywordSearchParams,
13+
type PineconeMetadataValue,
14+
type QueryParams,
15+
type SearchResult,
16+
} from '@will-cppa/pinecone-read-only-mcp';
17+
18+
export const DEMO_NAMESPACE = 'mailing';
19+
20+
const demoMetadata: Record<string, PineconeMetadataValue> = {
21+
document_number: 'D-100',
22+
title: 'Demo document',
23+
chunk_text: 'This is synthetic chunk text for the week-3 examples.',
24+
};
25+
26+
const demoHit: SearchResult = {
27+
id: 'demo-hit-1',
28+
content: String(demoMetadata['chunk_text']),
29+
score: 0.95,
30+
metadata: demoMetadata,
31+
reranked: true,
32+
};
33+
34+
export class DemoMockPineconeClient extends PineconeClient {
35+
constructor() {
36+
super({ apiKey: '00000000-0000-0000-0000-000000000000' });
37+
}
38+
39+
override async listNamespacesWithMetadata(): Promise<
40+
Array<{ namespace: string; recordCount: number; metadata: Record<string, string> }>
41+
> {
42+
return [
43+
{
44+
namespace: DEMO_NAMESPACE,
45+
recordCount: 42,
46+
metadata: {
47+
document_number: 'string',
48+
title: 'string',
49+
chunk_text: 'string',
50+
url: 'string',
51+
},
52+
},
53+
];
54+
}
55+
56+
override async listNamespacesFromKeywordIndex(): Promise<KeywordIndexNamespacesResult> {
57+
return {
58+
ok: true,
59+
namespaces: [{ namespace: DEMO_NAMESPACE, recordCount: 42 }],
60+
};
61+
}
62+
63+
override async checkIndexes(): Promise<{ ok: boolean; errors: string[] }> {
64+
return { ok: true, errors: [] };
65+
}
66+
67+
override async query(params: QueryParams): Promise<HybridQueryResult> {
68+
const reranked = params.useReranking !== false;
69+
const row: SearchResult = {
70+
...demoHit,
71+
reranked,
72+
metadata: { ...demoMetadata },
73+
};
74+
return {
75+
results: [row],
76+
degraded: false,
77+
hybrid_leg_failed: null,
78+
};
79+
}
80+
81+
override async count(_params: CountParams): Promise<CountResult> {
82+
return { count: 7, truncated: false };
83+
}
84+
85+
override async keywordSearch(_params: KeywordSearchParams): Promise<SearchResult[]> {
86+
return [];
87+
}
88+
}

examples/library-embedding-demo.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88
*
99
* **Single process:** `setupServer` registers tools against process-global
1010
* singletons (suggest-flow state, namespaces cache, URL registry, active config).
11-
* Do **not** call `setupServer` twice in one process for isolated tenants unless
12-
* you accept shared state — prefer **one server per Node process** or external
13-
* process isolation. (A future release may add an explicit teardown API; see
14-
* CHANGELOG when available.)
11+
* A second `setupServer` throws — call `teardownServer()` first to re-initialize
12+
* (tests). For isolated tenants in production, prefer one server per Node process.
1513
*/
1614

1715
import {

examples/mcp-linked-transport.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Minimal in-memory MCP transport pair for examples (no subprocess / stdio).
3+
* Each `send` delivers the JSON-RPC message to the peer's `onmessage` on a microtask.
4+
*/
5+
6+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
7+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
8+
9+
export function createLinkedTransports(): {
10+
clientTransport: Transport;
11+
serverTransport: Transport;
12+
} {
13+
let closed = false;
14+
const clientTransport: Transport = {
15+
onmessage: undefined,
16+
onclose: undefined,
17+
onerror: undefined,
18+
async start() {},
19+
async send(message: JSONRPCMessage) {
20+
queueMicrotask(() => {
21+
if (closed) return;
22+
serverTransport.onmessage?.(message);
23+
});
24+
},
25+
async close() {
26+
if (closed) return;
27+
closed = true;
28+
clientTransport.onclose?.();
29+
serverTransport.onclose?.();
30+
},
31+
};
32+
33+
const serverTransport: Transport = {
34+
onmessage: undefined,
35+
onclose: undefined,
36+
onerror: undefined,
37+
async start() {},
38+
async send(message: JSONRPCMessage) {
39+
queueMicrotask(() => {
40+
if (closed) return;
41+
clientTransport.onmessage?.(message);
42+
});
43+
},
44+
async close() {
45+
if (closed) return;
46+
closed = true;
47+
clientTransport.onclose?.();
48+
serverTransport.onclose?.();
49+
},
50+
};
51+
52+
return { clientTransport, serverTransport };
53+
}

examples/tsconfig.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"moduleResolution": "Node16",
6+
"strict": true,
7+
"noEmit": true,
8+
"skipLibCheck": true,
9+
"baseUrl": ".",
10+
"paths": {
11+
"@will-cppa/pinecone-read-only-mcp": ["../dist/server.js"]
12+
}
13+
},
14+
"include": ["./**/*.ts"]
15+
}

0 commit comments

Comments
 (0)