From 7e778bcc31de219cd146514b8fe915f1e96f622c Mon Sep 17 00:00:00 2001 From: zho Date: Tue, 26 May 2026 02:08:51 +0800 Subject: [PATCH 1/6] updated project structure --- .env.example | 9 +- CHANGELOG.md | 5 + README.md | 54 +++-- docs/CONFIGURATION.md | 22 +- examples/README.md | 8 +- examples/custom-url-generator.ts | 34 ++- examples/demo-mock-pinecone-client.ts | 2 +- examples/guided-query-demo.ts | 20 +- examples/library-embedding-demo.ts | 26 ++- examples/suggest-flow-demo.ts | 20 +- examples/tsconfig.json | 6 +- package.json | 14 +- scripts/test-search.ts | 2 +- src/alliance/config.test.ts | 56 +++++ src/alliance/config.ts | 37 ++++ src/alliance/index.ts | 18 ++ src/alliance/setup.ts | 24 ++ .../tools/guided-query-tool.test.ts | 38 +++- .../tools/guided-query-tool.ts | 30 +-- .../tools/suggest-query-params-tool.test.ts | 10 +- .../tools/suggest-query-params-tool.ts | 16 +- src/alliance/url-builtins.test.ts | 51 +++++ src/alliance/url-builtins.ts | 90 ++++++++ src/cli.ts | 8 +- src/constants.ts | 6 +- src/core/config.test.ts | 37 ++++ src/{ => core}/config.ts | 44 ++-- src/core/index.ts | 43 ++++ src/{ => core}/pinecone-client.test.ts | 40 +++- src/{ => core}/pinecone-client.ts | 32 +-- src/{ => core}/pinecone/indexes.test.ts | 2 +- src/{ => core}/pinecone/indexes.ts | 8 +- src/{ => core}/pinecone/rerank.test.ts | 2 +- src/{ => core}/pinecone/rerank.ts | 4 +- src/{ => core}/pinecone/search.test.ts | 2 +- src/{ => core}/pinecone/search.ts | 6 +- src/core/rerank-trace.test.ts | 34 +++ src/core/rerank-trace.ts | 20 ++ src/{ => core}/server.test.ts | 32 +-- src/{ => core}/server/client-context.ts | 0 src/{ => core}/server/config-context.ts | 8 +- .../server/format-query-result.test.ts | 4 +- src/{ => core}/server/format-query-result.ts | 6 +- src/{ => core}/server/metadata-filter.test.ts | 0 src/{ => core}/server/metadata-filter.ts | 0 src/{ => core}/server/namespace-router.ts | 0 src/{ => core}/server/namespace-utils.ts | 0 src/{ => core}/server/namespaces-cache.ts | 0 src/{ => core}/server/query-suggestion.ts | 2 +- .../server/reassemble-documents.test.ts | 4 +- src/{ => core}/server/reassemble-documents.ts | 4 +- src/{ => core}/server/retry.test.ts | 0 src/{ => core}/server/retry.ts | 0 src/{ => core}/server/suggestion-flow.ts | 0 src/{ => core}/server/tool-error.test.ts | 0 src/{ => core}/server/tool-error.ts | 2 +- src/{ => core}/server/tool-response.ts | 0 .../server/tools/count-tool.test.ts | 0 src/{ => core}/server/tools/count-tool.ts | 0 .../server/tools/generate-urls-tool.test.ts | 4 +- .../server/tools/generate-urls-tool.ts | 2 +- .../server/tools/keyword-search-tool.test.ts | 4 +- .../server/tools/keyword-search-tool.ts | 4 +- .../server/tools/list-namespaces-tool.test.ts | 0 .../server/tools/list-namespaces-tool.ts | 0 .../tools/namespace-router-tool.test.ts | 0 .../server/tools/namespace-router-tool.ts | 0 .../server/tools/query-documents-tool.test.ts | 2 +- .../server/tools/query-documents-tool.ts | 4 +- .../server/tools/query-tool.test.ts | 2 +- src/{ => core}/server/tools/query-tool.ts | 4 +- src/{ => core}/server/tools/test-helpers.ts | 5 +- src/core/server/url-registry.test.ts | 53 +++++ src/core/server/url-registry.ts | 102 +++++++++ src/core/setup.ts | 77 +++++++ src/index.ts | 22 +- src/logger.ts | 2 +- src/server.ts | 151 ------------- src/server/url-generation.test.ts | 145 ------------ src/server/url-generation.ts | 209 ------------------ src/types.ts | 17 +- 81 files changed, 1014 insertions(+), 737 deletions(-) create mode 100644 src/alliance/config.test.ts create mode 100644 src/alliance/config.ts create mode 100644 src/alliance/index.ts create mode 100644 src/alliance/setup.ts rename src/{server => alliance}/tools/guided-query-tool.test.ts (81%) rename src/{server => alliance}/tools/guided-query-tool.ts (90%) rename src/{server => alliance}/tools/suggest-query-params-tool.test.ts (93%) rename src/{server => alliance}/tools/suggest-query-params-tool.ts (85%) create mode 100644 src/alliance/url-builtins.test.ts create mode 100644 src/alliance/url-builtins.ts create mode 100644 src/core/config.test.ts rename src/{ => core}/config.ts (78%) create mode 100644 src/core/index.ts rename src/{ => core}/pinecone-client.test.ts (90%) rename src/{ => core}/pinecone-client.ts (87%) rename src/{ => core}/pinecone/indexes.test.ts (99%) rename src/{ => core}/pinecone/indexes.ts (97%) rename src/{ => core}/pinecone/rerank.test.ts (97%) rename src/{ => core}/pinecone/rerank.ts (95%) rename src/{ => core}/pinecone/search.test.ts (98%) rename src/{ => core}/pinecone/search.ts (97%) create mode 100644 src/core/rerank-trace.test.ts create mode 100644 src/core/rerank-trace.ts rename src/{ => core}/server.test.ts (81%) rename src/{ => core}/server/client-context.ts (100%) rename src/{ => core}/server/config-context.ts (65%) rename src/{ => core}/server/format-query-result.test.ts (95%) rename src/{ => core}/server/format-query-result.ts (94%) rename src/{ => core}/server/metadata-filter.test.ts (100%) rename src/{ => core}/server/metadata-filter.ts (100%) rename src/{ => core}/server/namespace-router.ts (100%) rename src/{ => core}/server/namespace-utils.ts (100%) rename src/{ => core}/server/namespaces-cache.ts (100%) rename src/{ => core}/server/query-suggestion.ts (98%) rename src/{ => core}/server/reassemble-documents.test.ts (98%) rename src/{ => core}/server/reassemble-documents.ts (97%) rename src/{ => core}/server/retry.test.ts (100%) rename src/{ => core}/server/retry.ts (100%) rename src/{ => core}/server/suggestion-flow.ts (100%) rename src/{ => core}/server/tool-error.test.ts (100%) rename src/{ => core}/server/tool-error.ts (98%) rename src/{ => core}/server/tool-response.ts (100%) rename src/{ => core}/server/tools/count-tool.test.ts (100%) rename src/{ => core}/server/tools/count-tool.ts (100%) rename src/{ => core}/server/tools/generate-urls-tool.test.ts (89%) rename src/{ => core}/server/tools/generate-urls-tool.ts (97%) rename src/{ => core}/server/tools/keyword-search-tool.test.ts (96%) rename src/{ => core}/server/tools/keyword-search-tool.ts (97%) rename src/{ => core}/server/tools/list-namespaces-tool.test.ts (100%) rename src/{ => core}/server/tools/list-namespaces-tool.ts (100%) rename src/{ => core}/server/tools/namespace-router-tool.test.ts (100%) rename src/{ => core}/server/tools/namespace-router-tool.ts (100%) rename src/{ => core}/server/tools/query-documents-tool.test.ts (99%) rename src/{ => core}/server/tools/query-documents-tool.ts (97%) rename src/{ => core}/server/tools/query-tool.test.ts (99%) rename src/{ => core}/server/tools/query-tool.ts (97%) rename src/{ => core}/server/tools/test-helpers.ts (94%) create mode 100644 src/core/server/url-registry.test.ts create mode 100644 src/core/server/url-registry.ts create mode 100644 src/core/setup.ts delete mode 100644 src/server.ts delete mode 100644 src/server/url-generation.test.ts delete mode 100644 src/server/url-generation.ts diff --git a/.env.example b/.env.example index 4cb373c..be94f69 100644 --- a/.env.example +++ b/.env.example @@ -3,10 +3,11 @@ # Required: Your Pinecone API key PINECONE_API_KEY=your-pinecone-api-key-here -# Optional: Pinecone index name (default: rag-hybrid) -PINECONE_INDEX_NAME=rag-hybrid +# Required: Dense (hybrid) Pinecone index name (no default) +PINECONE_INDEX_NAME= -# Optional: Reranking model (default: bge-reranker-v2-m3) +# Optional: Reranking model. Unset is OK for CLI / alliance — bge-reranker-v2-m3 is applied automatically. +# Core-only (setupCoreServer) with no env var: reranking disabled. PINECONE_RERANK_MODEL=bge-reranker-v2-m3 # Optional: Default number of results to return (default: 10) @@ -21,7 +22,7 @@ PINECONE_READ_ONLY_MCP_LOG_LEVEL=INFO PINECONE_READ_ONLY_MCP_LOG_FORMAT=text # Optional: Sparse index name (default: ${PINECONE_INDEX_NAME}-sparse) -# PINECONE_SPARSE_INDEX_NAME=rag-hybrid-sparse +# PINECONE_SPARSE_INDEX_NAME=my-index-sparse # Optional: Namespace + suggestion-flow cache TTL in seconds (default: 1800 = 30min) # PINECONE_CACHE_TTL_SECONDS=1800 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4439f0e..13cadb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ## [Unreleased] +### Changed + +- **Breaking:** `PINECONE_INDEX_NAME` (or `--index-name`) is required; hardcoded Alliance **index** default removed. Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). Core has **no** rerank default; CLI and `resolveAllianceConfig` / `setupAllianceServer` still apply `bge-reranker-v2-m3` when `PINECONE_RERANK_MODEL` is unset so key+index-only MCP configs keep reranking. +- Hybrid `query` / `guided_query` set `rerank_skipped_reason: no_model` and `decision_trace.rerank_status: skipped_no_model` when reranking was requested but no rerank model is configured (core-only embedders). `setupCoreServer` logs a WARN when rerank is disabled. + ### Added - `UrlGeneratorFn` type alias (same as `UrlGenerator`) and `RegisterBuiltinUrlGeneratorsOptions` with `reinstallBuiltins` on `registerBuiltinUrlGenerators()` to restore default `mailing` / `slack-Cpplang` generators after overrides; README “Custom URL generators” section and tests for custom registration and built-in override. diff --git a/README.md b/README.md index 5b79fc2..26e9e5f 100644 --- a/README.md +++ b/README.md @@ -84,16 +84,24 @@ npm install npm run build ``` +## Architecture + +The codebase is split into two layers: + +- **`src/core/`** — generic MCP–Pinecone bridge (`PineconeClient`, `resolveConfig`, core MCP tools). Import from `@will-cppa/pinecone-read-only-mcp` (package root). +- **`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 call `setupAllianceServer(config)` for the full tool surface (what the CLI uses). + ## Configuration -You need a **Pinecone API key** and (by default) a **dense** index plus matching **sparse** index; see [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every environment variable and CLI flag. +You need a **Pinecone API key** and a **dense index name** (`PINECONE_INDEX_NAME`); sparse index defaults to `{index}-sparse`. **CLI and `setupAllianceServer`** apply the Alliance rerank default (`bge-reranker-v2-m3`) when `PINECONE_RERANK_MODEL` is unset so existing key+index-only MCP configs keep reranking. **Generic core** (`setupCoreServer` / package root) has no rerank default — set `PINECONE_RERANK_MODEL` or pass `rerankModel` explicitly. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every variable and CLI flag. Quick reference: | Variable | Required | Default | | ----------------------------------- | ----------------------- | --------------------------------- | | `PINECONE_API_KEY` | Yes (for live Pinecone) | — | -| `PINECONE_INDEX_NAME` | No | `rag-hybrid` | +| `PINECONE_INDEX_NAME` | Yes | — (no default) | +| `PINECONE_RERANK_MODEL` | No (Alliance CLI default if unset) | Alliance: `bge-reranker-v2-m3`; core-only: disabled | | `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | | `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | | `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | No | `text` (`json` for log pipelines) | @@ -102,31 +110,37 @@ Run `pinecone-read-only-mcp --help` for CLI equivalents (`--cache-ttl-seconds`, ### Deployment model -The server uses **process-global** memory for the suggest-flow gate (`suggest_query_params` context), namespaces cache, URL generator registry, and active configuration. **Stdio MCP (one client per Node process)** matches this model. If you embed `setupServer` behind a multi-tenant HTTP transport, isolate those structures per session yourself or treat the suggest-flow guard as best-effort only. +The server uses **process-global** memory for the suggest-flow gate (`suggest_query_params` context), namespaces cache, URL generator registry, and active configuration. **Stdio MCP (one client per Node process)** matches this model. If you embed `setupAllianceServer` behind a multi-tenant HTTP transport, isolate those structures per session yourself or treat the suggest-flow guard as best-effort only. -### Library embedding (`setupServer`) +### Library embedding -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. +Treat **`setupCoreServer()` / `setupAllianceServer()` as one logical server per Node process**: they mutate shared module singletons (suggest-flow map, namespaces cache, URL registry, config context, shared `PineconeClient` slot). A **second** setup call in the same process **throws** unless you call **`teardownServer()`** first. -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. +- **Generic bridge only:** `import { setupCoreServer, teardownServer, ... } from '@will-cppa/pinecone-read-only-mcp'` +- **Full Alliance surface (CLI parity):** `import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'` -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). +Recommended pattern for the full tool surface: `resolveAllianceConfig({ apiKey, indexName, ... })` → `setPineconeClient(new PineconeClient(...))` → `await setupAllianceServer(config)` → connect one MCP transport. Use `resolveConfig` only for **core-only** embedding (no implicit rerank default). See [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate). ### Custom URL generators Namespaces other than `mailing` and `slack-Cpplang` (or different URL rules for any namespace) can use programmatic registration — no fork required. -Import `registerUrlGenerator` and types `UrlGeneratorFn` / `UrlGenerationResult` from `@will-cppa/pinecone-read-only-mcp`. Register **additional** namespaces before tools that emit URLs run (typically right after `setupServer` resolves config). To **replace** the built-in `mailing` or `slack-Cpplang` generators, call `registerUrlGenerator` **after** `setupServer`, because `setupServer` installs the defaults first. +Import `registerUrlGenerator` and types `UrlGeneratorFn` / `UrlGenerationResult` from `@will-cppa/pinecone-read-only-mcp`. Register **additional** namespaces before tools that emit URLs run. Built-in `mailing` / `slack-Cpplang` generators are installed by `setupAllianceServer` (not by `setupCoreServer`). ```ts import { registerUrlGenerator, - setupServer, + resolveConfig, + setPineconeClient, + PineconeClient, type UrlGenerationResult, type UrlGeneratorFn, } from '@will-cppa/pinecone-read-only-mcp'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; -const server = await setupServer(config); +const config = resolveConfig({ apiKey: '...', indexName: 'your-index' }); +setPineconeClient(new PineconeClient({ apiKey: config.apiKey, indexName: config.indexName })); +const server = await setupAllianceServer(config); const myDocs: UrlGeneratorFn = (metadata): UrlGenerationResult => { const id = typeof metadata.doc_id === 'string' ? metadata.doc_id : null; @@ -146,7 +160,7 @@ A fuller embedding sample lives in [examples/custom-url-generator.ts](examples/c | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | [examples/suggest-flow-demo.ts](examples/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow with namespace consistency notes. | | [examples/guided-query-demo.ts](examples/guided-query-demo.ts) | **guided_query** orchestration and how to read `decision_trace`. | -| [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) | Programmatic **setupServer** wiring without the CLI binary. | +| [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) | Programmatic **setupAllianceServer** wiring without the CLI binary. | | [examples/custom-url-generator.ts](examples/custom-url-generator.ts) | Custom **URL generator** registration for `generate_urls` / row enrichment. | Run with `npx tsx examples/.ts` from a checkout (requires valid Pinecone env for live paths). @@ -162,7 +176,9 @@ Add to your `claude_desktop_config.json`: "command": "npx", "args": ["-y", "@will-cppa/pinecone-read-only-mcp"], "env": { - "PINECONE_API_KEY": "your-api-key-here" + "PINECONE_API_KEY": "your-api-key-here", + "PINECONE_INDEX_NAME": "your-index-name" + "PINECONE_RERANK_MODEL": "your-rerank-model" } } } @@ -230,12 +246,14 @@ node node_modules/@will-cppa/pinecone-read-only-mcp/dist/index.js --api-key YOUR ``` --api-key TEXT Pinecone API key (or set PINECONE_API_KEY env var) ---index-name TEXT Pinecone index name [default: rag-hybrid] ---rerank-model TEXT Reranking model [default: bge-reranker-v2-m3] +--index-name TEXT Dense index (required, or PINECONE_INDEX_NAME) +--rerank-model TEXT Reranker model (optional; omit to disable reranking) --log-level TEXT Logging level [default: INFO] --help, -h Show help message ``` +Run `pinecone-read-only-mcp --help` for the full option list. + ## Deployment ### Production Readiness Defaults @@ -264,7 +282,7 @@ docker build -t pinecone-read-only-mcp:latest . # run (stdio MCP server) docker run --rm -i \ -e PINECONE_API_KEY=YOUR_API_KEY \ - -e PINECONE_INDEX_NAME=rag-hybrid \ + -e PINECONE_INDEX_NAME=your-index-name \ pinecone-read-only-mcp:latest ``` @@ -436,7 +454,7 @@ Returns the **unique document count** matching a metadata filter and semantic qu ### `keyword_search` -Performs **keyword (lexical/sparse-only)** search over the dedicated sparse index (default: `rag-hybrid-sparse`, i.e. `{PINECONE_INDEX_NAME}-sparse`). Use for exact or keyword-style queries. Does not use the dense index or semantic reranking. Call `list_namespaces` first to discover namespaces; `suggest_query_params` is optional. +Performs **keyword (lexical/sparse-only)** search over the sparse index (`{PINECONE_INDEX_NAME}-sparse` by default). Use for exact or keyword-style queries. Does not use the dense index or semantic reranking. Call `list_namespaces` first to discover namespaces; `suggest_query_params` is optional. **Parameters:** @@ -457,7 +475,7 @@ Performs **keyword (lexical/sparse-only)** search over the dedicated sparse inde "status": "success", "query": "contracts C++", "namespace": "wg21-papers", - "index": "rag-hybrid-sparse", + "index": "your-index-sparse", "result_count": 5, "results": [ { @@ -624,7 +642,7 @@ The script prints a table of p50, p95, and p99 latencies in milliseconds and wri PINECONE_API_KEY=your-key npm run test:search ``` - If the sparse index (`rag-hybrid-sparse` by default) does not exist or has no data, the keyword search step is skipped with a warning. + If the sparse index (`{PINECONE_INDEX_NAME}-sparse`) does not exist or has no data, the keyword search step is skipped with a warning. 2. **Via MCP client:** Start the server and call the `keyword_search` tool with `query_text`, `namespace` (from `list_namespaces`), and optional `top_k` or `metadata_filter`. Response shape is the same as the `query` tool (e.g. `results` with ids, metadata, scores; `reranked` is always `false`). diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index dc15d2d..76f363a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -6,7 +6,7 @@ Configuration is built from **CLI flags** (when using the binary), **environment **CLI / `ConfigOverrides` > environment variables > built-in defaults.** -`resolveConfig` in `src/config.ts` applies this order for each field. +`resolveConfig` in `src/core/config.ts` applies this order for each field. --- @@ -15,9 +15,9 @@ Configuration is built from **CLI flags** (when using the binary), **environment | Field | Source | Default / notes | | ----- | ------ | --------------- | | `apiKey` | `apiKey` / `PINECONE_API_KEY` | **Required** (non-empty after trim) | -| `indexName` | `indexName` / `PINECONE_INDEX_NAME` | `rag-hybrid` | +| `indexName` | `indexName` / `PINECONE_INDEX_NAME` | **Required** (no default) | | `sparseIndexName` | `sparseIndexName` / `PINECONE_SPARSE_INDEX_NAME` | `{indexName}-sparse` | -| `rerankModel` | `rerankModel` / `PINECONE_RERANK_MODEL` | `bge-reranker-v2-m3` | +| `rerankModel` | `rerankModel` / `PINECONE_RERANK_MODEL` | Core: unset disables reranking. Alliance CLI / `resolveAllianceConfig`: defaults to `bge-reranker-v2-m3` when unset | | `defaultTopK` | `defaultTopK` / `PINECONE_TOP_K` | `10` (positive int) | | `logLevel` | `logLevel` / `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | `INFO` (`DEBUG`–`ERROR`) | | `logFormat` | `logFormat` / `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | `text` or `json` | @@ -26,7 +26,15 @@ Configuration is built from **CLI flags** (when using the binary), **environment | `disableSuggestFlow` | `disableSuggestFlow` / `PINECONE_DISABLE_SUGGEST_FLOW` | `false` (bool parsing: true/1/yes/on) | | `checkIndexes` | `checkIndexes` / `PINECONE_CHECK_INDEXES` | `false` | -**Throws** if `apiKey` is missing after trim. +**Throws** if `apiKey` or `indexName` is missing after trim. + +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 call `setupAllianceServer(config)`. + +### Alliance rerank default + +`resolveConfig` (core) never invents a rerank model. **`resolveAllianceConfig`** and **`setupAllianceServer`** call `applyAllianceRerankDefault()` so deployments that only set `PINECONE_API_KEY` and `PINECONE_INDEX_NAME` (typical MCP configs) still use `bge-reranker-v2-m3`. Override with `PINECONE_RERANK_MODEL` or `--rerank-model` anytime. + +Generic embedders using only `setupCoreServer` should set `PINECONE_RERANK_MODEL` explicitly or accept disabled reranking (startup WARN). --- @@ -52,9 +60,9 @@ Configuration is built from **CLI flags** (when using the binary), **environment ## Library embedding -1. Build `ServerConfig` with `resolveConfig({ apiKey: '...', ... })` or pass explicit overrides. -2. Construct `PineconeClient` and `setPineconeClient(client)` before `setupServer(config)` (mirrors `src/index.ts`). -3. `await setupServer(config)` then connect an MCP transport. +1. Build `ServerConfig` with `resolveAllianceConfig({ apiKey: '...', indexName: '...', ... })` for the full surface, or `resolveConfig` for core-only (no rerank default). +2. Construct `PineconeClient` and `setPineconeClient(client)` before `setupAllianceServer(config)` (mirrors `src/index.ts`). +3. `await setupAllianceServer(config)` (or `setupCoreServer` for generic tools only) then connect an MCP transport. See [README deployment model](../README.md#deployment-model) and [examples/library-embedding-demo.ts](../examples/library-embedding-demo.ts). diff --git a/examples/README.md b/examples/README.md index c595b3f..a662de2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,9 +2,11 @@ | File | Description | |------|-------------| -| [custom-url-generator.ts](./custom-url-generator.ts) | Embed the MCP server as a library, register a **custom URL generator** for a namespace, and wire `PineconeClient` + `setupServer()`. | +| [custom-url-generator.ts](./custom-url-generator.ts) | Embed the MCP server, register a **custom URL generator**, and call `setupAllianceServer()` (full tool surface). | | [suggest-flow-demo.ts](./suggest-flow-demo.ts) | Document the **suggest_query_params → query** gate sequence and trimmed namespace usage. | | [guided-query-demo.ts](./guided-query-demo.ts) | Document **guided_query** and the **`decision_trace`** payload. | -| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveConfig`, `setPineconeClient`, `setupServer`). | +| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveConfig`, `setPineconeClient`, `setupAllianceServer`). | -Run with `npx tsx examples/.ts` after `npm install` (live Pinecone calls need `PINECONE_API_KEY` and related env). +**Required env for live runs:** `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`. Optional: `PINECONE_RERANK_MODEL` (omit to disable reranking). + +Run with `npm run build` then `npx tsx examples/.ts` from the repo root (examples resolve the package via `dist/core` and `dist/alliance`). diff --git a/examples/custom-url-generator.ts b/examples/custom-url-generator.ts index f461d4b..393a862 100644 --- a/examples/custom-url-generator.ts +++ b/examples/custom-url-generator.ts @@ -3,31 +3,29 @@ * * The Pinecone Read-Only MCP exposes a per-namespace URL registry so library * consumers can synthesize URLs from metadata when records do not already - * carry a `url` field. Built-ins cover `mailing` (Boost archives) and - * `slack-Cpplang`; everything else is up to you. + * carry a `url` field. Built-in `mailing` / `slack-Cpplang` generators are + * registered by `setupAllianceServer` (Alliance layer); everything else is up to you. * * Usage: - * 1. Import { registerUrlGenerator, setupServer, ... } from the package. - * 2. Call registerUrlGenerator(namespace, fn) BEFORE setupServer(config) so - * the registry is populated when the server registers tools. - * 3. The generate_urls tool, plus query/keyword_search/guided_query rows, - * will pick up the generator automatically. + * 1. Import `registerUrlGenerator` from `@will-cppa/pinecone-read-only-mcp`. + * 2. Import `setupAllianceServer` from `@will-cppa/pinecone-read-only-mcp/alliance`. + * 3. Call `registerUrlGenerator(namespace, fn)` before `setupAllianceServer(config)`. + * 4. The generate_urls tool and query row enrichment use the registry automatically. * - * Run from a project that depends on the package, or replace the import - * with a relative path when running inside this repo. + * Run from a project that depends on the package, or use this repo after `npm run build`. */ import { PineconeClient, registerUrlGenerator, - resolveConfig, setPineconeClient, - setupServer, type UrlGenerationResult, } from '@will-cppa/pinecone-read-only-mcp'; +import { + resolveAllianceConfig, + setupAllianceServer, +} from '@will-cppa/pinecone-read-only-mcp/alliance'; -// Example domain: a documentation index whose chunks carry { product, slug } -// metadata. We turn that into https://docs.example.com/{product}/{slug}. registerUrlGenerator('product-docs', (metadata): UrlGenerationResult => { const product = typeof metadata['product'] === 'string' ? metadata['product'] : null; const slug = typeof metadata['slug'] === 'string' ? metadata['slug'] : null; @@ -45,7 +43,10 @@ registerUrlGenerator('product-docs', (metadata): UrlGenerationResult => { }); async function main(): Promise { - const config = resolveConfig({ apiKey: process.env['PINECONE_API_KEY'] ?? 'demo-key' }); + const apiKey = process.env['PINECONE_API_KEY']?.trim(); + const indexName = process.env['PINECONE_INDEX_NAME']?.trim() ?? 'demo-index'; + const config = resolveAllianceConfig({ apiKey: apiKey ?? 'demo-key', indexName }); + setPineconeClient( new PineconeClient({ apiKey: config.apiKey, @@ -56,10 +57,7 @@ async function main(): Promise { }) ); - const server = await setupServer(config); - // `server` is ready to connect to a transport. The generate_urls tool - // (and any query result enrichment) will route `product-docs` records - // through the generator registered above. + const server = await setupAllianceServer(config); void server; console.log('Custom URL generator registered for namespace "product-docs".'); } diff --git a/examples/demo-mock-pinecone-client.ts b/examples/demo-mock-pinecone-client.ts index 9592062..b2b0939 100644 --- a/examples/demo-mock-pinecone-client.ts +++ b/examples/demo-mock-pinecone-client.ts @@ -33,7 +33,7 @@ const demoHit: SearchResult = { export class DemoMockPineconeClient extends PineconeClient { constructor() { - super({ apiKey: '00000000-0000-0000-0000-000000000000' }); + super({ apiKey: '00000000-0000-0000-0000-000000000000', indexName: 'demo-index' }); } override async listNamespacesWithMetadata(): Promise< diff --git a/examples/guided-query-demo.ts b/examples/guided-query-demo.ts index f95e3f2..6dbeb96 100644 --- a/examples/guided-query-demo.ts +++ b/examples/guided-query-demo.ts @@ -10,29 +10,29 @@ * selected namespace, suggested fields/tools, and the final `selected_tool`. * Use the trace in UIs or logs to explain why a path was chosen. * - * **Reranking fidelity:** when reranking was expected, inspect each row's + * **Reranking fidelity:** when a rerank model is configured, inspect each row's * `reranked` boolean; `false` can indicate rerank was skipped or failed while * still returning HTTP/MCP success (see docs/TOOLS.md). */ +import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - PineconeClient, - resolveConfig, - setPineconeClient, - setupServer, -} from '@will-cppa/pinecone-read-only-mcp'; + resolveAllianceConfig, + setupAllianceServer, +} from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); - if (!apiKey) { + const indexName = process.env['PINECONE_INDEX_NAME']?.trim(); + if (!apiKey || !indexName) { console.log( - '[guided-query-demo] Set PINECONE_API_KEY to run live. ' + + '[guided-query-demo] Set PINECONE_API_KEY and PINECONE_INDEX_NAME to run live. ' + 'Call guided_query with user_query; read decision_trace + result in the JSON response.' ); return; } - const config = resolveConfig({ apiKey }); + const config = resolveAllianceConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ apiKey: config.apiKey, @@ -44,7 +44,7 @@ async function main(): Promise { }) ); - const server = await setupServer(config); + const server = await setupAllianceServer(config); void server; console.log('Server ready — call guided_query({ user_query, preferred_tool?: "auto" }).'); } diff --git a/examples/library-embedding-demo.ts b/examples/library-embedding-demo.ts index 1c60edc..e73d7c5 100644 --- a/examples/library-embedding-demo.ts +++ b/examples/library-embedding-demo.ts @@ -2,22 +2,21 @@ * Library embedding: build the MCP server from a Node script (not the CLI). * * Pattern (mirrors `src/index.ts`): - * 1. `resolveConfig({ apiKey, ... })` — never rely on ambient env alone in libraries. + * 1. `resolveAllianceConfig({ apiKey, indexName, ... })` for full surface (includes rerank default). * 2. `new PineconeClient({ ... })` + `setPineconeClient(client)`. - * 3. `await setupServer(config)` then `server.connect(transport)`. + * 3. `await setupAllianceServer(config)` then `server.connect(transport)`. * - * **Single process:** `setupServer` registers tools against process-global + * **Single process:** `setupAllianceServer` registers tools against process-global * singletons (suggest-flow state, namespaces cache, URL registry, active config). - * A second `setupServer` throws — call `teardownServer()` first to re-initialize + * A second setup call throws — call `teardownServer()` first to re-initialize * (tests). For isolated tenants in production, prefer one server per Node process. */ +import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - PineconeClient, - resolveConfig, - setPineconeClient, - setupServer, -} from '@will-cppa/pinecone-read-only-mcp'; + resolveAllianceConfig, + setupAllianceServer, +} from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); @@ -27,7 +26,12 @@ async function main(): Promise { ); return; } - const config = resolveConfig({ apiKey }); + const indexName = process.env['PINECONE_INDEX_NAME']?.trim(); + if (!indexName) { + console.log('Set PINECONE_INDEX_NAME to run this example. Skipping live setup in doc-only mode.'); + return; + } + const config = resolveAllianceConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ @@ -40,7 +44,7 @@ async function main(): Promise { }) ); - const server = await setupServer(config); + const server = await setupAllianceServer(config); // const transport = new StdioServerTransport(); // await server.connect(transport); void server; diff --git a/examples/suggest-flow-demo.ts b/examples/suggest-flow-demo.ts index 24ce76d..84f30a2 100644 --- a/examples/suggest-flow-demo.ts +++ b/examples/suggest-flow-demo.ts @@ -13,27 +13,27 @@ * optional `fields` from `suggested_fields`. * * This file is runnable without Pinecone only in **documentation mode**; set - * `PINECONE_API_KEY` and wire an MCP transport to execute real tool calls. + * `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`, then wire an MCP transport. */ +import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - PineconeClient, - resolveConfig, - setPineconeClient, - setupServer, -} from '@will-cppa/pinecone-read-only-mcp'; + resolveAllianceConfig, + setupAllianceServer, +} from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); - if (!apiKey) { + const indexName = process.env['PINECONE_INDEX_NAME']?.trim(); + if (!apiKey || !indexName) { console.log( - '[suggest-flow-demo] Set PINECONE_API_KEY to run against Pinecone. ' + + '[suggest-flow-demo] Set PINECONE_API_KEY and PINECONE_INDEX_NAME to run against Pinecone. ' + 'Flow: list_namespaces → suggest_query_params → query (same trimmed namespace).' ); return; } - const config = resolveConfig({ apiKey }); + const config = resolveAllianceConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ apiKey: config.apiKey, @@ -45,7 +45,7 @@ async function main(): Promise { }) ); - const server = await setupServer(config); + const server = await setupAllianceServer(config); // With an MCP client connected to `server`, invoke tools in order: // 1) suggest_query_params({ namespace: "mailing".trim(), user_query: "..." }) // 2) query({ query_text, namespace: "mailing", preset: "detailed", ... }) diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 0c20ccd..f20f454 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -4,11 +4,13 @@ "module": "Node16", "moduleResolution": "Node16", "strict": true, + "ignoreDeprecations": "6.0", "noEmit": true, "skipLibCheck": true, - "baseUrl": ".", + "types": ["node"], "paths": { - "@will-cppa/pinecone-read-only-mcp": ["../dist/server.js"] + "@will-cppa/pinecone-read-only-mcp": ["../src/core/index.ts"], + "@will-cppa/pinecone-read-only-mcp/alliance": ["../src/alliance/index.ts"] } }, "include": ["./**/*.ts"] diff --git a/package.json b/package.json index adb61f0..576e933 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,17 @@ "version": "0.1.6", "description": "A Model Context Protocol (MCP) server that provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking.", "type": "module", - "main": "dist/server.js", - "module": "dist/server.js", - "types": "dist/server.d.ts", + "main": "dist/core/index.js", + "module": "dist/core/index.js", + "types": "dist/core/index.d.ts", "exports": { ".": { - "types": "./dist/server.d.ts", - "import": "./dist/server.js" + "types": "./dist/core/index.d.ts", + "import": "./dist/core/index.js" + }, + "./alliance": { + "types": "./dist/alliance/index.d.ts", + "import": "./dist/alliance/index.js" }, "./package.json": "./package.json" }, diff --git a/scripts/test-search.ts b/scripts/test-search.ts index 449f78c..0294b2b 100644 --- a/scripts/test-search.ts +++ b/scripts/test-search.ts @@ -167,7 +167,7 @@ async function test() { `⚠️ Keyword search skipped: sparse index has no namespaces (or index unavailable).` ); console.log( - ` Ensure the sparse index (e.g. rag-hybrid-sparse) exists and has data.` + ` Ensure the sparse index (PINECONE_INDEX_NAME-sparse) exists and has data.` ); } else { const sparseTestNamespace = sparseNamespaces[0].namespace; diff --git a/src/alliance/config.test.ts b/src/alliance/config.test.ts new file mode 100644 index 0000000..11d3c2c --- /dev/null +++ b/src/alliance/config.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { + applyAllianceRerankDefault, + DEFAULT_ALLIANCE_RERANK_MODEL, + resolveAllianceConfig, +} from './config.js'; + +describe('resolveAllianceConfig', () => { + it('applies Alliance rerank default when env and overrides omit rerankModel', () => { + const cfg = resolveAllianceConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + expect(cfg.rerankModel).toBe(DEFAULT_ALLIANCE_RERANK_MODEL); + }); + + it('preserves explicit rerankModel from overrides', () => { + const cfg = resolveAllianceConfig({ + apiKey: 'sk-test', + indexName: 'my-index', + rerankModel: 'custom-reranker', + }); + expect(cfg.rerankModel).toBe('custom-reranker'); + }); + + it('preserves rerankModel from env over Alliance default', () => { + const cfg = resolveAllianceConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { + PINECONE_API_KEY: 'sk-test', + PINECONE_INDEX_NAME: 'my-index', + PINECONE_RERANK_MODEL: 'env-reranker', + } + ); + expect(cfg.rerankModel).toBe('env-reranker'); + }); +}); + +describe('applyAllianceRerankDefault', () => { + it('does not replace an existing rerankModel', () => { + const cfg = applyAllianceRerankDefault({ + apiKey: 'k', + indexName: 'i', + sparseIndexName: 'i-sparse', + rerankModel: 'already-set', + defaultTopK: 10, + logLevel: 'INFO', + logFormat: 'text', + cacheTtlMs: 1800_000, + requestTimeoutMs: 15_000, + disableSuggestFlow: false, + checkIndexes: false, + }); + expect(cfg.rerankModel).toBe('already-set'); + }); +}); diff --git a/src/alliance/config.ts b/src/alliance/config.ts new file mode 100644 index 0000000..47aacd4 --- /dev/null +++ b/src/alliance/config.ts @@ -0,0 +1,37 @@ +/** + * Alliance-layer configuration: wraps {@link resolveConfig} with deployment defaults + * that must not live in the generic core package. + */ + +import { + resolveConfig, + type ConfigOverrides, + type ServerConfig, +} from '../core/config.js'; + +/** Default Pinecone inference rerank model for C++ Alliance deployments (CLI / alliance entry). */ +export const DEFAULT_ALLIANCE_RERANK_MODEL = 'bge-reranker-v2-m3'; + +/** + * Apply the Alliance rerank default when no model was provided via env or overrides. + * Core {@link resolveConfig} intentionally leaves `rerankModel` unset so generic adopters + * do not silently use an Alliance-specific model name. + */ +export function applyAllianceRerankDefault(config: ServerConfig): ServerConfig { + if (config.rerankModel !== undefined) { + return config; + } + return { ...config, rerankModel: DEFAULT_ALLIANCE_RERANK_MODEL }; +} + +/** + * Build {@link ServerConfig} for the full Alliance MCP surface (CLI and `setupAllianceServer`). + * Same requirements as core (`apiKey`, `indexName`); supplies {@link DEFAULT_ALLIANCE_RERANK_MODEL} + * when `PINECONE_RERANK_MODEL` and `rerankModel` overrides are absent. + */ +export function resolveAllianceConfig( + overrides: ConfigOverrides = {}, + env: NodeJS.ProcessEnv = process.env +): ServerConfig { + return applyAllianceRerankDefault(resolveConfig(overrides, env)); +} diff --git a/src/alliance/index.ts b/src/alliance/index.ts new file mode 100644 index 0000000..659b47c --- /dev/null +++ b/src/alliance/index.ts @@ -0,0 +1,18 @@ +/** + * @packageDocumentation + * **@will-cppa/pinecone-read-only-mcp/alliance** — full server including Alliance app tools. + */ + +export * from '../core/index.js'; +export { + applyAllianceRerankDefault, + DEFAULT_ALLIANCE_RERANK_MODEL, + resolveAllianceConfig, +} from './config.js'; +export { setupAllianceServer } from './setup.js'; +export { + registerBuiltinUrlGenerators, + generatorMailing, + generatorSlackCpplang, +} from './url-builtins.js'; +export type { RegisterBuiltinUrlGeneratorsOptions } from './url-builtins.js'; diff --git a/src/alliance/setup.ts b/src/alliance/setup.ts new file mode 100644 index 0000000..d52c50e --- /dev/null +++ b/src/alliance/setup.ts @@ -0,0 +1,24 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ServerConfig } from '../core/config.js'; +import { resolveConfig } from '../core/config.js'; +import { setupCoreServer } from '../core/setup.js'; +import { applyAllianceRerankDefault } from './config.js'; +import { registerBuiltinUrlGenerators } from './url-builtins.js'; +import { registerGuidedQueryTool } from './tools/guided-query-tool.js'; +import { registerSuggestQueryParamsTool } from './tools/suggest-query-params-tool.js'; + +/** + * Create and configure the MCP server with the full Alliance tool surface: + * all core tools plus `suggest_query_params`, `guided_query`, and built-in URL generators. + * + * When `config` is omitted, resolves env via {@link resolveConfig} then applies the + * Alliance rerank default. When `config` is passed, applies the default if `rerankModel` is unset. + */ +export async function setupAllianceServer(config?: ServerConfig): Promise { + const allianceConfig = applyAllianceRerankDefault(config ?? resolveConfig({})); + const server = await setupCoreServer(allianceConfig); + registerBuiltinUrlGenerators(); + registerSuggestQueryParamsTool(server); + registerGuidedQueryTool(server); + return server; +} diff --git a/src/server/tools/guided-query-tool.test.ts b/src/alliance/tools/guided-query-tool.test.ts similarity index 81% rename from src/server/tools/guided-query-tool.test.ts rename to src/alliance/tools/guided-query-tool.test.ts index b1e8073..bcc5b68 100644 --- a/src/server/tools/guided-query-tool.test.ts +++ b/src/alliance/tools/guided-query-tool.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getPineconeClient } from '../client-context.js'; -import { getNamespacesWithCache } from '../namespaces-cache.js'; +import { getPineconeClient } from '../../core/server/client-context.js'; +import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; import { registerGuidedQueryTool } from './guided-query-tool.js'; import { assertToolErrorCode, @@ -9,18 +9,18 @@ import { makeNamespaceCacheEntry, makeSearchResult, parseToolJson, -} from './test-helpers.js'; +} from '../../core/server/tools/test-helpers.js'; -vi.mock('../client-context.js', () => ({ +vi.mock('../../core/server/client-context.js', () => ({ getPineconeClient: vi.fn(), })); -vi.mock('../namespaces-cache.js', () => ({ +vi.mock('../../core/server/namespaces-cache.js', () => ({ getNamespacesWithCache: vi.fn(), })); /** Real `markSuggested` may call `getServerConfig()` during sweep (CI has no API key); isolate the handler. */ -vi.mock('../suggestion-flow.js', () => ({ +vi.mock('../../core/server/suggestion-flow.js', () => ({ markSuggested: vi.fn(), })); @@ -81,6 +81,32 @@ describe('guided_query tool handler', () => { expect(result.degradation_reason).toBe('rerank_failed: timeout'); }); + it('guided_query: reports skipped_no_model when rerank was requested but no model configured', async () => { + mockedGetClient.mockReturnValue({ + query: vi.fn().mockResolvedValue( + makeHybridQueryResult({ + rerank_skipped_reason: 'no_model', + degradation_reason: 'rerank_skipped_no_model: set PINECONE_RERANK_MODEL', + }) + ), + count: vi.fn().mockResolvedValue({ count: 7, truncated: false }), + } as never); + + const server = createMockServer(); + registerGuidedQueryTool(server as never); + + const body = parseToolJson( + await server.getHandler('guided_query')!({ + user_query: 'What does the paper say about contracts?', + namespace: 'papers', + preferred_tool: 'detailed', + }) + ); + + const trace = body.decision_trace as Record; + expect(trace.rerank_status).toBe('skipped_no_model'); + }); + it('runs query_detailed path on auto when user asks for content', async () => { const server = createMockServer(); registerGuidedQueryTool(server as never); diff --git a/src/server/tools/guided-query-tool.ts b/src/alliance/tools/guided-query-tool.ts similarity index 90% rename from src/server/tools/guided-query-tool.ts rename to src/alliance/tools/guided-query-tool.ts index a127a04..ab3ff9b 100644 --- a/src/server/tools/guided-query-tool.ts +++ b/src/alliance/tools/guided-query-tool.ts @@ -1,22 +1,26 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; +import { guidedRerankStatus } from '../../core/rerank-trace.js'; import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../constants.js'; import type { QueryResponse } from '../../types.js'; -import { getPineconeClient } from '../client-context.js'; -import { formatQueryResultRows } from '../format-query-result.js'; -import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js'; -import { rankNamespacesByQuery } from '../namespace-router.js'; -import { getNamespacesWithCache } from '../namespaces-cache.js'; -import { normalizeNamespace } from '../namespace-utils.js'; -import { suggestQueryParams } from '../query-suggestion.js'; -import { markSuggested } from '../suggestion-flow.js'; +import { getPineconeClient } from '../../core/server/client-context.js'; +import { formatQueryResultRows } from '../../core/server/format-query-result.js'; +import { + metadataFilterSchema, + validateMetadataFilterDetailed, +} from '../../core/server/metadata-filter.js'; +import { rankNamespacesByQuery } from '../../core/server/namespace-router.js'; +import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; +import { normalizeNamespace } from '../../core/server/namespace-utils.js'; +import { suggestQueryParams } from '../../core/server/query-suggestion.js'; +import { markSuggested } from '../../core/server/suggestion-flow.js'; import { classifyToolCatchError, logToolError, pineconeToolError, validationToolError, -} from '../tool-error.js'; -import { jsonErrorResponse, jsonResponse } from '../tool-response.js'; +} from '../../core/server/tool-error.js'; +import { jsonErrorResponse, jsonResponse } from '../../core/server/tool-response.js'; type GuidedToolName = 'count' | 'fast' | 'detailed' | 'full'; @@ -210,11 +214,7 @@ export function registerGuidedQueryTool(server: McpServer): void { useReranking: !isFast, fields: fields?.length ? fields : undefined, }); - const rerank_status: 'success' | 'skipped' | 'failed' = isFast - ? 'skipped' - : queryOutcome.degraded && queryOutcome.hybrid_leg_failed === null - ? 'failed' - : 'success'; + const rerank_status = guidedRerankStatus(!isFast, queryOutcome); const formattedResults = formatQueryResultRows(queryOutcome.results, { namespace, enrichUrls: enrich_urls, diff --git a/src/server/tools/suggest-query-params-tool.test.ts b/src/alliance/tools/suggest-query-params-tool.test.ts similarity index 93% rename from src/server/tools/suggest-query-params-tool.test.ts rename to src/alliance/tools/suggest-query-params-tool.test.ts index 5882a55..9f7035c 100644 --- a/src/server/tools/suggest-query-params-tool.test.ts +++ b/src/alliance/tools/suggest-query-params-tool.test.ts @@ -1,19 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getNamespacesWithCache } from '../namespaces-cache.js'; -import { markSuggested } from '../suggestion-flow.js'; +import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; +import { markSuggested } from '../../core/server/suggestion-flow.js'; import { registerSuggestQueryParamsTool } from './suggest-query-params-tool.js'; import { createMockServer, makeNamespaceCacheEntry, parseToolJson, assertToolErrorCode, -} from './test-helpers.js'; +} from '../../core/server/tools/test-helpers.js'; -vi.mock('../namespaces-cache.js', () => ({ +vi.mock('../../core/server/namespaces-cache.js', () => ({ getNamespacesWithCache: vi.fn(), })); -vi.mock('../suggestion-flow.js', () => ({ +vi.mock('../../core/server/suggestion-flow.js', () => ({ markSuggested: vi.fn(), })); diff --git a/src/server/tools/suggest-query-params-tool.ts b/src/alliance/tools/suggest-query-params-tool.ts similarity index 85% rename from src/server/tools/suggest-query-params-tool.ts rename to src/alliance/tools/suggest-query-params-tool.ts index 461a40e..1c21232 100644 --- a/src/server/tools/suggest-query-params-tool.ts +++ b/src/alliance/tools/suggest-query-params-tool.ts @@ -1,11 +1,15 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { normalizeNamespace } from '../namespace-utils.js'; -import { getNamespacesWithCache } from '../namespaces-cache.js'; -import { suggestQueryParams } from '../query-suggestion.js'; -import { markSuggested } from '../suggestion-flow.js'; -import { classifyToolCatchError, logToolError, validationToolError } from '../tool-error.js'; -import { jsonErrorResponse, jsonResponse } from '../tool-response.js'; +import { normalizeNamespace } from '../../core/server/namespace-utils.js'; +import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; +import { suggestQueryParams } from '../../core/server/query-suggestion.js'; +import { markSuggested } from '../../core/server/suggestion-flow.js'; +import { + classifyToolCatchError, + logToolError, + validationToolError, +} from '../../core/server/tool-error.js'; +import { jsonErrorResponse, jsonResponse } from '../../core/server/tool-response.js'; /** Register the suggest_query_params tool on the MCP server. */ export function registerSuggestQueryParamsTool(server: McpServer): void { diff --git a/src/alliance/url-builtins.test.ts b/src/alliance/url-builtins.test.ts new file mode 100644 index 0000000..c4eea94 --- /dev/null +++ b/src/alliance/url-builtins.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, beforeAll, afterEach } from 'vitest'; +import { generateUrlForNamespace } from '../core/server/url-registry.js'; +import { registerBuiltinUrlGenerators } from './url-builtins.js'; +import { registerUrlGenerator, unregisterUrlGenerator } from '../core/server/url-registry.js'; + +beforeAll(() => { + registerBuiltinUrlGenerators(); +}); + +describe('generateUrlForNamespace (alliance builtins)', () => { + it('generates mailing URL from doc_id', () => { + const r = generateUrlForNamespace('mailing', { + doc_id: 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6', + }); + expect(r.url).toBe( + 'https://lists.boost.org/archives/list/boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6/' + ); + expect(r.method).toBe('generated.mailing'); + }); + + it('generates slack URL from team/channel/doc_id', () => { + const r = generateUrlForNamespace('slack-Cpplang', { + team_id: 'T123456789', + channel_id: 'C123456', + doc_id: '1234567.890', + }); + expect(r.url).toBe('https://app.slack.com/client/T123456789/C123456/p1234567890'); + expect(r.method).toBe('generated.slack'); + }); +}); + +describe('registerBuiltinUrlGenerators', () => { + const customNs = 'acme-docs'; + + afterEach(() => { + unregisterUrlGenerator(customNs); + registerBuiltinUrlGenerators({ reinstallBuiltins: true }); + }); + + it('allows a custom generator to override the mailing built-in', () => { + registerUrlGenerator('mailing', () => ({ + url: 'https://override.example/mailing', + method: 'generated.custom', + })); + const r = generateUrlForNamespace('mailing', { + doc_id: 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6', + }); + expect(r.url).toBe('https://override.example/mailing'); + expect(r.method).toBe('generated.custom'); + }); +}); diff --git a/src/alliance/url-builtins.ts b/src/alliance/url-builtins.ts new file mode 100644 index 0000000..ae2ea13 --- /dev/null +++ b/src/alliance/url-builtins.ts @@ -0,0 +1,90 @@ +/** + * C++ Alliance domain-specific URL generators (Boost mailing list, Slack). + */ + +import { registerUrlGenerator } from '../core/server/url-registry.js'; +import type { UrlGenerationResult } from '../core/server/url-registry.js'; + +/** Return a trimmed non-empty string or null for empty/missing values. */ +function asString(value: unknown): string | null { + return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; +} + +/** + * Build a mailing-list URL (e.g. Boost archives). + */ +export function generatorMailing(metadata: Record): UrlGenerationResult { + const listName = asString(metadata['list_name']); + const docId = asString(metadata['doc_id']) ?? asString(metadata['msg_id']); + const threadId = asString(metadata['thread_id']); + + if (listName && docId && !docId.includes(listName)) { + return { + url: `https://lists.boost.org/archives/list/${listName}/message/${docId}/`, + method: 'generated.mailing', + }; + } + + const docIdOrThread = docId ?? threadId; + if (!docIdOrThread) { + return { + url: null, + method: 'unavailable', + reason: 'mailing requires doc_id, msg_id, or thread_id to generate URL', + }; + } + return { + url: `https://lists.boost.org/archives/list/${docIdOrThread}/`, + method: 'generated.mailing', + }; +} + +/** Build a Slack message URL from source or team_id/channel_id/doc_id. */ +export function generatorSlackCpplang(metadata: Record): UrlGenerationResult { + const source = asString(metadata['source']); + if (source) { + return { url: source, method: 'metadata.source' }; + } + const teamId = asString(metadata['team_id']); + const channelId = asString(metadata['channel_id']); + const docId = asString(metadata['doc_id']); + if (!teamId || !channelId || !docId) { + return { + url: null, + method: 'unavailable', + reason: 'slack-Cpplang requires team_id, channel_id, and doc_id (or source)', + }; + } + const messageId = docId.replace(/\./g, ''); + return { + url: `https://app.slack.com/client/${teamId}/${channelId}/p${messageId}`, + method: 'generated.slack', + }; +} + +let builtinGeneratorsRegistered = false; + +/** Options for {@link registerBuiltinUrlGenerators}. */ +export type RegisterBuiltinUrlGeneratorsOptions = { + /** + * When `true`, re-applies the built-in `mailing` and `slack-Cpplang` generators + * even if they were already registered or were replaced by {@link registerUrlGenerator}. + */ + reinstallBuiltins?: boolean; +}; + +/** + * Register built-in Alliance generators (`mailing`, `slack-Cpplang`). + */ +export function registerBuiltinUrlGenerators(options?: RegisterBuiltinUrlGeneratorsOptions): void { + if (options?.reinstallBuiltins) { + registerUrlGenerator('mailing', generatorMailing); + registerUrlGenerator('slack-Cpplang', generatorSlackCpplang); + builtinGeneratorsRegistered = true; + return; + } + if (builtinGeneratorsRegistered) return; + registerUrlGenerator('mailing', generatorMailing); + registerUrlGenerator('slack-Cpplang', generatorSlackCpplang); + builtinGeneratorsRegistered = true; +} diff --git a/src/cli.ts b/src/cli.ts index 6123a23..6dd5a46 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,8 +3,8 @@ * for {@link resolveConfig}; environment variables remain the fallback there. */ -import { DEFAULT_INDEX_NAME, DEFAULT_RERANK_MODEL, SERVER_VERSION } from './constants.js'; -import type { ConfigOverrides } from './config.js'; +import { SERVER_VERSION } from './constants.js'; +import type { ConfigOverrides } from './core/config.js'; export type ParseCliResult = | { kind: 'help' } @@ -136,9 +136,9 @@ Usage: pinecone-read-only-mcp [options] Options: --api-key TEXT Pinecone API key (or PINECONE_API_KEY) - --index-name TEXT Dense index [default: ${DEFAULT_INDEX_NAME}] + --index-name TEXT Dense index (required, or PINECONE_INDEX_NAME) --sparse-index-name TEXT Sparse index [default: {index-name}-sparse] - --rerank-model TEXT Reranker model [default: ${DEFAULT_RERANK_MODEL}] + --rerank-model TEXT Reranker model (optional; omit to disable reranking) --top-k N Default top-k for queries [env: PINECONE_TOP_K] --log-level LEVEL DEBUG | INFO | WARN | ERROR [default: INFO] --log-format FORMAT text | json [default: text] diff --git a/src/constants.ts b/src/constants.ts index 449e536..fad6ffd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,8 +2,6 @@ * Constants for Pinecone Read-Only MCP */ -export const DEFAULT_INDEX_NAME = 'rag-hybrid'; -export const DEFAULT_RERANK_MODEL = 'bge-reranker-v2-m3'; export const DEFAULT_TOP_K = 10; export const MAX_TOP_K = 100; export const MIN_TOP_K = 1; @@ -40,13 +38,13 @@ A semantic search server that provides hybrid search capabilities over Pinecone Features: - Hybrid Search: Combines dense and sparse embeddings for superior recall -- Semantic Reranking: Uses a configurable reranker model (default bge-reranker-v2-m3) for improved precision +- Semantic Reranking: Uses a configurable reranker model when PINECONE_RERANK_MODEL is set; disabled when unset - Dynamic Namespace Discovery: Automatically discovers available namespaces - Metadata Filtering: Supports optional metadata filters for refined searches - Namespace Router: Suggests likely namespace(s) from natural-language intent - Count: Use the count tool for "how many X?" questions; it uses semantic search only and minimal fields (no content) for performance, returning unique document count. - URL Generation: Use generate_urls to synthesize URLs for namespaces that have a registered generator when metadata lacks url. -- Document reassembly: Use query_documents to get whole documents (chunks grouped and merged by document_number/doc_id/url) for content analysis or summarization. query_documents always reranks for best document-level relevance. +- Document reassembly: Use query_documents to get whole documents (chunks grouped and merged by document_number/doc_id/url) for content analysis or summarization. query_documents reranks when a rerank model is configured. - Keyword search: Use keyword_search to query the sparse index for lexical/keyword-only retrieval without reranking. Usage: diff --git a/src/core/config.test.ts b/src/core/config.test.ts new file mode 100644 index 0000000..8e5a3a8 --- /dev/null +++ b/src/core/config.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { resolveConfig } from './config.js'; + +describe('resolveConfig', () => { + it('throws when API key is missing', () => { + expect(() => resolveConfig({}, { PINECONE_API_KEY: '' })).toThrow(/Missing Pinecone API key/); + }); + + it('throws when index name is missing', () => { + expect(() => + resolveConfig({ apiKey: 'sk-test' }, { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: '' }) + ).toThrow(/Missing Pinecone index name/); + }); + + it('requires index from overrides when env is unset', () => { + expect(() => resolveConfig({ apiKey: 'sk-test' }, {})).toThrow(/Missing Pinecone index name/); + }); + + it('leaves rerankModel undefined when env and overrides omit it', () => { + const cfg = resolveConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + expect(cfg.indexName).toBe('my-index'); + expect(cfg.sparseIndexName).toBe('my-index-sparse'); + expect(cfg.rerankModel).toBeUndefined(); + }); + + it('sets rerankModel when provided', () => { + const cfg = resolveConfig({ + apiKey: 'sk-test', + indexName: 'my-index', + rerankModel: 'my-reranker', + }); + expect(cfg.rerankModel).toBe('my-reranker'); + }); +}); diff --git a/src/config.ts b/src/core/config.ts similarity index 78% rename from src/config.ts rename to src/core/config.ts index 9872b2c..85571c3 100644 --- a/src/config.ts +++ b/src/core/config.ts @@ -2,16 +2,11 @@ * Shared runtime config types and resolver. * * `ServerConfig` is the single source of truth flowed from `parseCli()` → - * `setupServer(config)` → every collaborator. Modules MUST NOT read - * `process.env` directly anymore — they receive their slice of the config. + * `setupCoreServer(config)` / `setupAllianceServer(config)` → every collaborator. + * Modules MUST NOT read `process.env` directly anymore — they receive their slice of the config. */ -import { - DEFAULT_INDEX_NAME, - DEFAULT_RERANK_MODEL, - DEFAULT_TOP_K, - FLOW_CACHE_TTL_MS, -} from './constants.js'; +import { DEFAULT_TOP_K, FLOW_CACHE_TTL_MS } from '../constants.js'; /** Allowed log levels, in ascending severity. */ export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'; @@ -23,18 +18,17 @@ export type LogFormat = 'text' | 'json'; * Unified runtime configuration for the MCP server. * * Built once by `parseCli()` (or constructed directly by library consumers) - * and threaded through `setupServer(config)`. All optional knobs have safe - * defaults; only `apiKey` is required. + * and threaded through setup. `apiKey` and `indexName` are required. */ export interface ServerConfig { /** Pinecone API key. Required. */ apiKey: string; - /** Dense (hybrid) index name. Defaults to `DEFAULT_INDEX_NAME`. */ + /** Dense (hybrid) index name. Required. */ indexName: string; /** Sparse index name. Defaults to `${indexName}-sparse`. */ sparseIndexName: string; - /** Reranker model identifier. Defaults to `DEFAULT_RERANK_MODEL`. */ - rerankModel: string; + /** Reranker model identifier. When unset, reranking is disabled. */ + rerankModel?: string; /** Default top-k when callers omit it on `query`. */ defaultTopK: number; /** Minimum log level emitted to stderr. */ @@ -77,6 +71,12 @@ function asBool(value: string | undefined, fallback: boolean): boolean { return fallback; } +function trimOptional(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const t = value.trim(); + return t.length > 0 ? t : undefined; +} + /** Partial config used by `resolveConfig` (CLI overrides for env). */ export interface ConfigOverrides { apiKey?: string; @@ -96,7 +96,7 @@ export interface ConfigOverrides { * Build a `ServerConfig` from CLI overrides, environment variables, and defaults. * CLI > env > default precedence is preserved. * - * @throws Error when no API key is provided (empty after trim from overrides or `PINECONE_API_KEY`). + * @throws Error when no API key or index name is provided. */ export function resolveConfig( overrides: ConfigOverrides, @@ -108,10 +108,18 @@ export function resolveConfig( 'Missing Pinecone API key: set PINECONE_API_KEY or pass --api-key (or apiKey in ConfigOverrides for library use).' ); } - const indexName = overrides.indexName ?? env['PINECONE_INDEX_NAME'] ?? DEFAULT_INDEX_NAME; + + const indexName = trimOptional(overrides.indexName ?? env['PINECONE_INDEX_NAME']); + if (!indexName) { + throw new Error('Missing Pinecone index name: set PINECONE_INDEX_NAME or pass --index-name.'); + } + const sparseIndexName = - overrides.sparseIndexName ?? env['PINECONE_SPARSE_INDEX_NAME'] ?? `${indexName}-sparse`; - const rerankModel = overrides.rerankModel ?? env['PINECONE_RERANK_MODEL'] ?? DEFAULT_RERANK_MODEL; + trimOptional(overrides.sparseIndexName ?? env['PINECONE_SPARSE_INDEX_NAME']) ?? + `${indexName}-sparse`; + + const rerankModel = trimOptional(overrides.rerankModel ?? env['PINECONE_RERANK_MODEL']); + const defaultTopK = overrides.defaultTopK ?? asPositiveInt(env['PINECONE_TOP_K'], DEFAULT_TOP_K); const logLevel = asLogLevel( overrides.logLevel ?? env['PINECONE_READ_ONLY_MCP_LOG_LEVEL'], @@ -135,7 +143,7 @@ export function resolveConfig( apiKey, indexName, sparseIndexName, - rerankModel, + ...(rerankModel !== undefined ? { rerankModel } : {}), defaultTopK, logLevel, logFormat, diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..3fdf7d5 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,43 @@ +/** + * @packageDocumentation + * **@will-cppa/pinecone-read-only-mcp** — generic (core) programmatic entrypoint. + * + * Import from the package root for the generic MCP-Pinecone bridge only. + * For the full Alliance tool surface, use `@will-cppa/pinecone-read-only-mcp/alliance`. + */ + +export { setPineconeClient } from './server/client-context.js'; +export { + validateMetadataFilter, + validateMetadataFilterDetailed, +} from './server/metadata-filter.js'; +export type { MetadataFilterValidationError } from './server/metadata-filter.js'; +export { toolErrorSchema } from './server/tool-error.js'; +export type { ToolError, ToolErrorCode } from './server/tool-error.js'; +export { suggestQueryParams } from './server/query-suggestion.js'; +export type { RecommendedTool, SuggestQueryParamsResult } from './server/query-suggestion.js'; +export { + registerUrlGenerator, + unregisterUrlGenerator, + generateUrlForNamespace, + hasUrlGenerator, +} from './server/url-registry.js'; +export type { UrlGenerationResult, UrlGenerator, UrlGeneratorFn } from './server/url-registry.js'; +export { resolveConfig } from './config.js'; +export type { ServerConfig, LogLevel, LogFormat, ConfigOverrides } from './config.js'; +export { PineconeClient } from './pinecone-client.js'; +export type { + PineconeClientConfig, + QueryParams, + CountParams, + CountResult, + KeywordSearchParams, + SearchResult, + PineconeMetadataValue, + QueryResponse, + QueryResultRowShape, + KeywordIndexNamespacesResult, + HybridQueryResult, + HybridLegFailed, +} from '../types.js'; +export { setupCoreServer, teardownServer } from './setup.js'; diff --git a/src/pinecone-client.test.ts b/src/core/pinecone-client.test.ts similarity index 90% rename from src/pinecone-client.test.ts rename to src/core/pinecone-client.test.ts index a101cb0..aec4edd 100644 --- a/src/pinecone-client.test.ts +++ b/src/core/pinecone-client.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PineconeClient } from './pinecone-client.js'; import { resolveConfig } from './config.js'; -import type { SearchableIndex, PineconeHit } from './types.js'; +import type { SearchableIndex, PineconeHit } from '../types.js'; import * as rerankModule from './pinecone/rerank.js'; /** Stubs for private methods (assigned at runtime; avoid intersecting private `PineconeClient` members). */ @@ -265,6 +265,44 @@ describe('PineconeClient', () => { expect(fieldsPassed).toContain('title'); }); + it('skips rerank API when rerankModel is not configured even if useReranking is true', async () => { + const noRerankClient = new PineconeClient({ + apiKey: 'test-api-key', + indexName: 'test-index', + }); + const spy = vi.spyOn(rerankModule, 'rerankResults'); + try { + const testClient = stubPineconeClient(noRerankClient); + const denseRef = {} as SearchableIndex; + const sparseRef = {} as SearchableIndex; + testClient.ensureIndexes = async () => ({ + denseIndex: denseRef, + sparseIndex: sparseRef, + }); + testClient.searchIndex = async (index) => { + if (index === denseRef) { + return [{ _id: 'd1', _score: 0.9, fields: { chunk_text: 'plain' } }]; + } + return []; + }; + + const results = await noRerankClient.query({ + query: 'q', + namespace: 'n', + topK: 5, + useReranking: true, + }); + + expect(results.results).toHaveLength(1); + expect(results.results[0]?.reranked).toBe(false); + expect(results.rerank_skipped_reason).toBe('no_model'); + expect(results.degradation_reason).toMatch(/rerank_skipped_no_model/); + expect(spy).not.toHaveBeenCalled(); + } finally { + spy.mockRestore(); + } + }); + it('uses rerankResults from pinecone/rerank when useReranking is true', async () => { const spy = vi.spyOn(rerankModule, 'rerankResults').mockResolvedValue({ results: [ diff --git a/src/pinecone-client.ts b/src/core/pinecone-client.ts similarity index 87% rename from src/pinecone-client.ts rename to src/core/pinecone-client.ts index 3efde17..7699226 100644 --- a/src/pinecone-client.ts +++ b/src/core/pinecone-client.ts @@ -1,6 +1,6 @@ /** Hybrid dense+sparse query client with optional reranking (facade over `src/pinecone/*`). */ -import { error as logError, info as logInfo } from './logger.js'; +import { error as logError, info as logInfo } from '../logger.js'; import type { PineconeClientConfig, SearchResult, @@ -13,15 +13,8 @@ import type { SearchableIndex, HybridQueryResult, HybridLegFailed, -} from './types.js'; -import { - DEFAULT_INDEX_NAME, - DEFAULT_RERANK_MODEL, - DEFAULT_TOP_K, - MAX_TOP_K, - COUNT_TOP_K, - COUNT_FIELDS, -} from './constants.js'; +} from '../types.js'; +import { DEFAULT_TOP_K, MAX_TOP_K, COUNT_TOP_K, COUNT_FIELDS } from '../constants.js'; import { PineconeIndexSession } from './pinecone/indexes.js'; import { countUniqueDocumentsFromHits, @@ -33,7 +26,7 @@ import { import { rerankResults as rerankResultsImpl } from './pinecone/rerank.js'; export class PineconeClient { - private rerankModel: string; + private readonly rerankModel: string | undefined; private defaultTopK: number; private readonly indexSession: PineconeIndexSession; @@ -43,9 +36,8 @@ export class PineconeClient { * built via {@link resolveConfig} / CLI); this class does not read `process.env`. */ constructor(config: PineconeClientConfig) { - const indexName = config.indexName ?? DEFAULT_INDEX_NAME; - this.indexSession = new PineconeIndexSession(config.apiKey, indexName); - this.rerankModel = config.rerankModel ?? DEFAULT_RERANK_MODEL; + this.indexSession = new PineconeIndexSession(config.apiKey, config.indexName); + this.rerankModel = config.rerankModel; this.defaultTopK = config.defaultTopK ?? DEFAULT_TOP_K; } @@ -123,8 +115,9 @@ export class PineconeClient { const topK = this.clampTopK(requestedTopK); + const effectiveReranking = useReranking && this.rerankModel !== undefined; const searchFields = - requestedFields?.length && useReranking && !requestedFields.includes('chunk_text') + requestedFields?.length && effectiveReranking && !requestedFields.includes('chunk_text') ? [...requestedFields, 'chunk_text'] : requestedFields; @@ -169,8 +162,9 @@ export class PineconeClient { let degraded = false; let degradation_reason: string | undefined; + let rerank_skipped_reason: 'no_model' | undefined; let documents: SearchResult[]; - if (useReranking) { + if (effectiveReranking && this.rerankModel) { const rerankOut = await rerankResultsImpl( this.indexSession.ensureClient(), this.rerankModel, @@ -183,6 +177,11 @@ export class PineconeClient { degradation_reason = rerankOut.degradation_reason; } else { documents = sliceMergedHitsToSearchResults(mergedResults, topK); + if (useReranking && this.rerankModel === undefined) { + rerank_skipped_reason = 'no_model'; + degradation_reason = + 'rerank_skipped_no_model: set PINECONE_RERANK_MODEL, pass rerankModel in config, or use resolveAllianceConfig / setupAllianceServer for the Alliance default rerank model.'; + } } logInfo( @@ -194,6 +193,7 @@ export class PineconeClient { degraded, ...(degradation_reason !== undefined ? { degradation_reason } : {}), hybrid_leg_failed: hybridLegFailed, + ...(rerank_skipped_reason !== undefined ? { rerank_skipped_reason } : {}), }; } diff --git a/src/pinecone/indexes.test.ts b/src/core/pinecone/indexes.test.ts similarity index 99% rename from src/pinecone/indexes.test.ts rename to src/core/pinecone/indexes.test.ts index a70d93b..47520fa 100644 --- a/src/pinecone/indexes.test.ts +++ b/src/core/pinecone/indexes.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { PineconeIndexSession } from './indexes.js'; -import type { SearchableIndex } from '../types.js'; +import type { SearchableIndex } from '../../types.js'; /** Subclass so tests inject index handles without calling the real Pinecone SDK. */ class PineconeIndexSessionTestDouble extends PineconeIndexSession { diff --git a/src/pinecone/indexes.ts b/src/core/pinecone/indexes.ts similarity index 97% rename from src/pinecone/indexes.ts rename to src/core/pinecone/indexes.ts index e1af568..60dbcc0 100644 --- a/src/pinecone/indexes.ts +++ b/src/core/pinecone/indexes.ts @@ -3,8 +3,12 @@ */ import { Pinecone } from '@pinecone-database/pinecone'; -import { error as logError, info as logInfo } from '../logger.js'; -import type { KeywordIndexNamespacesResult, NamespaceHandle, SearchableIndex } from '../types.js'; +import { error as logError, info as logInfo } from '../../logger.js'; +import type { + KeywordIndexNamespacesResult, + NamespaceHandle, + SearchableIndex, +} from '../../types.js'; /** * Infers a human-readable metadata field type for namespace discovery. diff --git a/src/pinecone/rerank.test.ts b/src/core/pinecone/rerank.test.ts similarity index 97% rename from src/pinecone/rerank.test.ts rename to src/core/pinecone/rerank.test.ts index 518f5ad..c26c63e 100644 --- a/src/pinecone/rerank.test.ts +++ b/src/core/pinecone/rerank.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { rerankResults } from './rerank.js'; -import type { MergedHit } from '../types.js'; +import type { MergedHit } from '../../types.js'; const sampleMerged: MergedHit[] = [ { _id: '1', _score: 0.5, chunk_text: 'hello', metadata: { k: 'v' } }, diff --git a/src/pinecone/rerank.ts b/src/core/pinecone/rerank.ts similarity index 95% rename from src/pinecone/rerank.ts rename to src/core/pinecone/rerank.ts index a712691..e39194d 100644 --- a/src/pinecone/rerank.ts +++ b/src/core/pinecone/rerank.ts @@ -3,8 +3,8 @@ */ import type { Pinecone } from '@pinecone-database/pinecone'; -import { error as logError } from '../logger.js'; -import type { MergedHit, SearchResult } from '../types.js'; +import { error as logError } from '../../logger.js'; +import type { MergedHit, SearchResult } from '../../types.js'; export type RerankOutcome = { results: SearchResult[]; diff --git a/src/pinecone/search.test.ts b/src/core/pinecone/search.test.ts similarity index 98% rename from src/pinecone/search.test.ts rename to src/core/pinecone/search.test.ts index 98bdbe9..31f322a 100644 --- a/src/pinecone/search.test.ts +++ b/src/core/pinecone/search.test.ts @@ -6,7 +6,7 @@ import { mapSparseHitsToSearchResults, countUniqueDocumentsFromHits, } from './search.js'; -import type { PineconeHit, SearchableIndex } from '../types.js'; +import type { PineconeHit, SearchableIndex } from '../../types.js'; describe('searchIndex', () => { it('uses index.search when available and passes fields', async () => { diff --git a/src/pinecone/search.ts b/src/core/pinecone/search.ts similarity index 97% rename from src/pinecone/search.ts rename to src/core/pinecone/search.ts index 030631e..44ebe0e 100644 --- a/src/pinecone/search.ts +++ b/src/core/pinecone/search.ts @@ -2,8 +2,8 @@ * Pinecone search pipeline: single-index text query and dense/sparse merge. */ -import { COUNT_FIELDS, COUNT_TOP_K } from '../constants.js'; -import { debug as logDebug, warn as logWarn } from '../logger.js'; +import { COUNT_FIELDS, COUNT_TOP_K } from '../../constants.js'; +import { debug as logDebug, warn as logWarn } from '../../logger.js'; import type { CountResult, MergedHit, @@ -11,7 +11,7 @@ import type { PineconeMetadataValue, SearchResult, SearchableIndex, -} from '../types.js'; +} from '../../types.js'; /** * Search a Pinecone index using text query with optional metadata filtering. diff --git a/src/core/rerank-trace.test.ts b/src/core/rerank-trace.test.ts new file mode 100644 index 0000000..1233c8a --- /dev/null +++ b/src/core/rerank-trace.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { guidedRerankStatus } from './rerank-trace.js'; +import { makeHybridQueryResult } from './server/tools/test-helpers.js'; + +describe('guidedRerankStatus', () => { + it('returns skipped when rerank was not requested', () => { + expect(guidedRerankStatus(false, makeHybridQueryResult())).toBe('skipped'); + }); + + it('returns skipped_no_model when outcome reports no rerank model', () => { + expect( + guidedRerankStatus( + true, + makeHybridQueryResult({ rerank_skipped_reason: 'no_model' }) + ) + ).toBe('skipped_no_model'); + }); + + it('returns failed when rerank degraded without hybrid leg failure', () => { + expect( + guidedRerankStatus( + true, + makeHybridQueryResult({ + degraded: true, + degradation_reason: 'rerank_failed: timeout', + }) + ) + ).toBe('failed'); + }); + + it('returns success when rerank was requested and not skipped or failed', () => { + expect(guidedRerankStatus(true, makeHybridQueryResult())).toBe('success'); + }); +}); diff --git a/src/core/rerank-trace.ts b/src/core/rerank-trace.ts new file mode 100644 index 0000000..71b2cf9 --- /dev/null +++ b/src/core/rerank-trace.ts @@ -0,0 +1,20 @@ +import type { HybridQueryResult } from '../types.js'; + +export type GuidedRerankStatus = 'success' | 'skipped' | 'skipped_no_model' | 'failed'; + +/** Map hybrid query outcome to guided_query `decision_trace.rerank_status`. */ +export function guidedRerankStatus( + requestedRerank: boolean, + outcome: HybridQueryResult +): GuidedRerankStatus { + if (!requestedRerank) { + return 'skipped'; + } + if (outcome.rerank_skipped_reason === 'no_model') { + return 'skipped_no_model'; + } + if (outcome.degraded && outcome.hybrid_leg_failed === null) { + return 'failed'; + } + return 'success'; +} diff --git a/src/server.test.ts b/src/core/server.test.ts similarity index 81% rename from src/server.test.ts rename to src/core/server.test.ts index 3da3982..7f1d406 100644 --- a/src/server.test.ts +++ b/src/core/server.test.ts @@ -2,13 +2,14 @@ import { describe, expect, it, afterEach } from 'vitest'; import { validateMetadataFilter, suggestQueryParams, - setupServer, + setupCoreServer, teardownServer, setPineconeClient, resolveConfig, PineconeClient, hasUrlGenerator, -} from './server.js'; +} from './index.js'; +import { setupAllianceServer } from '../alliance/setup.js'; describe('suggestQueryParams', () => { const wg21Fields = { @@ -106,13 +107,13 @@ describe('validateMetadataFilter', () => { }); }); -describe('setupServer lifecycle', () => { +describe('setupCoreServer lifecycle', () => { afterEach(() => { teardownServer(); }); - it('throws on second setupServer without teardown', async () => { - const cfg = resolveConfig({ apiKey: 'lifecycle-test-key' }); + it('throws on second setupCoreServer without teardown', async () => { + const cfg = resolveConfig({ apiKey: 'lifecycle-test-key', indexName: 'test-index' }); setPineconeClient( new PineconeClient({ apiKey: cfg.apiKey, @@ -121,12 +122,12 @@ describe('setupServer lifecycle', () => { defaultTopK: cfg.defaultTopK, }) ); - await setupServer(cfg); - await expect(setupServer(cfg)).rejects.toThrow(/teardownServer/); + await setupCoreServer(cfg); + await expect(setupCoreServer(cfg)).rejects.toThrow(/teardownServer/); }); - it('allows setup after teardown and reinstalls built-in URL generators', async () => { - const cfg = resolveConfig({ apiKey: 'lifecycle-test-key-2' }); + it('core setup does not register Alliance built-in URL generators', async () => { + const cfg = resolveConfig({ apiKey: 'lifecycle-test-key-2', indexName: 'test-index' }); setPineconeClient( new PineconeClient({ apiKey: cfg.apiKey, @@ -135,10 +136,13 @@ describe('setupServer lifecycle', () => { defaultTopK: cfg.defaultTopK, }) ); - await setupServer(cfg); - expect(hasUrlGenerator('mailing')).toBe(true); - teardownServer(); + await setupCoreServer(cfg); expect(hasUrlGenerator('mailing')).toBe(false); + teardownServer(); + }); + + it('alliance setup registers built-in URL generators after teardown', async () => { + const cfg = resolveConfig({ apiKey: 'lifecycle-test-key-3', indexName: 'test-index' }); setPineconeClient( new PineconeClient({ apiKey: cfg.apiKey, @@ -147,7 +151,9 @@ describe('setupServer lifecycle', () => { defaultTopK: cfg.defaultTopK, }) ); - await setupServer(cfg); + await setupAllianceServer(cfg); expect(hasUrlGenerator('mailing')).toBe(true); + teardownServer(); + expect(hasUrlGenerator('mailing')).toBe(false); }); }); diff --git a/src/server/client-context.ts b/src/core/server/client-context.ts similarity index 100% rename from src/server/client-context.ts rename to src/core/server/client-context.ts diff --git a/src/server/config-context.ts b/src/core/server/config-context.ts similarity index 65% rename from src/server/config-context.ts rename to src/core/server/config-context.ts index 42ed419..00e1cd8 100644 --- a/src/server/config-context.ts +++ b/src/core/server/config-context.ts @@ -3,7 +3,7 @@ import { resolveConfig } from '../config.js'; let activeConfig: ServerConfig | null = null; -/** Replace the process-global server config (called from `setupServer` with CLI/env-derived config). */ +/** Replace the process-global server config (called from setup with CLI/env-derived config). */ export function setServerConfig(config: ServerConfig): void { activeConfig = config; } @@ -17,9 +17,9 @@ export function resetServerConfig(): void { * Active server config for modules that cannot receive `ServerConfig` through parameters * (namespace cache TTL, suggest-flow gate, etc.). * - * When `setupServer()` runs without an explicit config, falls back to `resolveConfig({})` - * so env defaults still apply. That requires `PINECONE_API_KEY` (or the call throws); embedders - * should pass `config` into `setupServer(config)` when env is not set. + * When setup runs without an explicit config, falls back to `resolveConfig({})` + * (requires `PINECONE_API_KEY` and `PINECONE_INDEX_NAME` or the call throws). Embedders + * should pass `config` into `setupCoreServer(config)` / `setupAllianceServer(config)`. */ export function getServerConfig(): ServerConfig { if (!activeConfig) { diff --git a/src/server/format-query-result.test.ts b/src/core/server/format-query-result.test.ts similarity index 95% rename from src/server/format-query-result.test.ts rename to src/core/server/format-query-result.test.ts index d5da2f9..9c440a1 100644 --- a/src/server/format-query-result.test.ts +++ b/src/core/server/format-query-result.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as loggerModule from '../logger.js'; -import type { SearchResult } from '../types.js'; +import * as loggerModule from '../../logger.js'; +import type { SearchResult } from '../../types.js'; import { formatQueryResultRows, formatSearchResultAsRow, diff --git a/src/server/format-query-result.ts b/src/core/server/format-query-result.ts similarity index 94% rename from src/server/format-query-result.ts rename to src/core/server/format-query-result.ts index 5c01aa4..700f5a5 100644 --- a/src/server/format-query-result.ts +++ b/src/core/server/format-query-result.ts @@ -3,9 +3,9 @@ * Used by query tool and guided_query to avoid duplicated identifier/title/author/url logic. */ -import type { PineconeMetadataValue, SearchResult } from '../types.js'; -import { warn as logWarn } from '../logger.js'; -import { generateUrlForNamespace } from './url-generation.js'; +import type { PineconeMetadataValue, SearchResult } from '../../types.js'; +import { warn as logWarn } from '../../logger.js'; +import { generateUrlForNamespace } from './url-registry.js'; const DEFAULT_CONTENT_MAX_LENGTH = 2000; diff --git a/src/server/metadata-filter.test.ts b/src/core/server/metadata-filter.test.ts similarity index 100% rename from src/server/metadata-filter.test.ts rename to src/core/server/metadata-filter.test.ts diff --git a/src/server/metadata-filter.ts b/src/core/server/metadata-filter.ts similarity index 100% rename from src/server/metadata-filter.ts rename to src/core/server/metadata-filter.ts diff --git a/src/server/namespace-router.ts b/src/core/server/namespace-router.ts similarity index 100% rename from src/server/namespace-router.ts rename to src/core/server/namespace-router.ts diff --git a/src/server/namespace-utils.ts b/src/core/server/namespace-utils.ts similarity index 100% rename from src/server/namespace-utils.ts rename to src/core/server/namespace-utils.ts diff --git a/src/server/namespaces-cache.ts b/src/core/server/namespaces-cache.ts similarity index 100% rename from src/server/namespaces-cache.ts rename to src/core/server/namespaces-cache.ts diff --git a/src/server/query-suggestion.ts b/src/core/server/query-suggestion.ts similarity index 98% rename from src/server/query-suggestion.ts rename to src/core/server/query-suggestion.ts index f02dca8..0146969 100644 --- a/src/server/query-suggestion.ts +++ b/src/core/server/query-suggestion.ts @@ -1,4 +1,4 @@ -import { COUNT_FIELDS } from '../constants.js'; +import { COUNT_FIELDS } from '../../constants.js'; /** Values returned by `suggest_query_params` / suggestion flow; align with `query` tool `preset`. */ export type RecommendedTool = 'count' | 'fast' | 'detailed' | 'full'; diff --git a/src/server/reassemble-documents.test.ts b/src/core/server/reassemble-documents.test.ts similarity index 98% rename from src/server/reassemble-documents.test.ts rename to src/core/server/reassemble-documents.test.ts index b14d086..c96af8e 100644 --- a/src/server/reassemble-documents.test.ts +++ b/src/core/server/reassemble-documents.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as loggerModule from '../logger.js'; +import * as loggerModule from '../../logger.js'; import { reassembleByDocument } from './reassemble-documents.js'; -import type { SearchResult } from '../types.js'; +import type { SearchResult } from '../../types.js'; describe('reassembleByDocument', () => { let warnSpy: ReturnType; diff --git a/src/server/reassemble-documents.ts b/src/core/server/reassemble-documents.ts similarity index 97% rename from src/server/reassemble-documents.ts rename to src/core/server/reassemble-documents.ts index 54aafec..6897475 100644 --- a/src/server/reassemble-documents.ts +++ b/src/core/server/reassemble-documents.ts @@ -4,8 +4,8 @@ * for content analysis (summarization, full-document Q&A, etc.). */ -import type { PineconeMetadataValue, SearchResult } from '../types.js'; -import { warn as logWarn } from '../logger.js'; +import type { PineconeMetadataValue, SearchResult } from '../../types.js'; +import { warn as logWarn } from '../../logger.js'; /** Default metadata keys tried for chunk ordering (RecursiveCharacterTextSplitter often adds these). */ const CHUNK_ORDER_KEYS = ['chunk_index', 'start_index', 'loc'] as const; diff --git a/src/server/retry.test.ts b/src/core/server/retry.test.ts similarity index 100% rename from src/server/retry.test.ts rename to src/core/server/retry.test.ts diff --git a/src/server/retry.ts b/src/core/server/retry.ts similarity index 100% rename from src/server/retry.ts rename to src/core/server/retry.ts diff --git a/src/server/suggestion-flow.ts b/src/core/server/suggestion-flow.ts similarity index 100% rename from src/server/suggestion-flow.ts rename to src/core/server/suggestion-flow.ts diff --git a/src/server/tool-error.test.ts b/src/core/server/tool-error.test.ts similarity index 100% rename from src/server/tool-error.test.ts rename to src/core/server/tool-error.test.ts diff --git a/src/server/tool-error.ts b/src/core/server/tool-error.ts similarity index 98% rename from src/server/tool-error.ts rename to src/core/server/tool-error.ts index d506f96..dffe42d 100644 --- a/src/server/tool-error.ts +++ b/src/core/server/tool-error.ts @@ -4,7 +4,7 @@ */ import { z } from 'zod'; -import { getLogLevel, error as logError } from '../logger.js'; +import { getLogLevel, error as logError } from '../../logger.js'; /** User-facing error message: detailed in DEBUG, generic otherwise. */ export function getToolErrorMessage(error: unknown, fallbackMessage: string): string { diff --git a/src/server/tool-response.ts b/src/core/server/tool-response.ts similarity index 100% rename from src/server/tool-response.ts rename to src/core/server/tool-response.ts diff --git a/src/server/tools/count-tool.test.ts b/src/core/server/tools/count-tool.test.ts similarity index 100% rename from src/server/tools/count-tool.test.ts rename to src/core/server/tools/count-tool.test.ts diff --git a/src/server/tools/count-tool.ts b/src/core/server/tools/count-tool.ts similarity index 100% rename from src/server/tools/count-tool.ts rename to src/core/server/tools/count-tool.ts diff --git a/src/server/tools/generate-urls-tool.test.ts b/src/core/server/tools/generate-urls-tool.test.ts similarity index 89% rename from src/server/tools/generate-urls-tool.test.ts rename to src/core/server/tools/generate-urls-tool.test.ts index d08e640..74e1b75 100644 --- a/src/server/tools/generate-urls-tool.test.ts +++ b/src/core/server/tools/generate-urls-tool.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as urlGeneration from '../url-generation.js'; +import * as urlRegistry from '../url-registry.js'; import { registerGenerateUrlsTool } from './generate-urls-tool.js'; import { assertToolErrorCode, createMockServer } from './test-helpers.js'; @@ -20,7 +20,7 @@ describe('generate_urls tool handler', () => { }); it('returns PINECONE_ERROR when generateUrlForNamespace throws', async () => { - vi.spyOn(urlGeneration, 'generateUrlForNamespace').mockImplementation(() => { + vi.spyOn(urlRegistry, 'generateUrlForNamespace').mockImplementation(() => { throw new Error('generator boom'); }); const server = createMockServer(); diff --git a/src/server/tools/generate-urls-tool.ts b/src/core/server/tools/generate-urls-tool.ts similarity index 97% rename from src/server/tools/generate-urls-tool.ts rename to src/core/server/tools/generate-urls-tool.ts index 34006da..8ee5e2d 100644 --- a/src/server/tools/generate-urls-tool.ts +++ b/src/core/server/tools/generate-urls-tool.ts @@ -1,6 +1,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { generateUrlForNamespace } from '../url-generation.js'; +import { generateUrlForNamespace } from '../url-registry.js'; import { normalizeNamespace } from '../namespace-utils.js'; import { classifyToolCatchError, logToolError, validationToolError } from '../tool-error.js'; import { jsonErrorResponse, jsonResponse } from '../tool-response.js'; diff --git a/src/server/tools/keyword-search-tool.test.ts b/src/core/server/tools/keyword-search-tool.test.ts similarity index 96% rename from src/server/tools/keyword-search-tool.test.ts rename to src/core/server/tools/keyword-search-tool.test.ts index 96e6f21..576d4cb 100644 --- a/src/server/tools/keyword-search-tool.test.ts +++ b/src/core/server/tools/keyword-search-tool.test.ts @@ -14,7 +14,7 @@ describe('keyword_search tool handler', () => { vi.clearAllMocks(); mockedGetClient.mockReturnValue({ keywordSearch: vi.fn().mockResolvedValue([makeSearchResult()]), - getSparseIndexName: () => 'rag-hybrid-sparse', + getSparseIndexName: () => 'test-index-sparse', } as never); }); @@ -77,7 +77,7 @@ describe('keyword_search tool handler', () => { it('returns PINECONE_ERROR when keywordSearch throws', async () => { mockedGetClient.mockReturnValue({ keywordSearch: vi.fn().mockRejectedValue(new Error('sparse error')), - getSparseIndexName: () => 'rag-hybrid-sparse', + getSparseIndexName: () => 'test-index-sparse', } as never); const server = createMockServer(); registerKeywordSearchTool(server as never); diff --git a/src/server/tools/keyword-search-tool.ts b/src/core/server/tools/keyword-search-tool.ts similarity index 97% rename from src/server/tools/keyword-search-tool.ts rename to src/core/server/tools/keyword-search-tool.ts index 36e2fa5..d7e2b89 100644 --- a/src/server/tools/keyword-search-tool.ts +++ b/src/core/server/tools/keyword-search-tool.ts @@ -1,6 +1,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { MAX_TOP_K, MIN_TOP_K } from '../../constants.js'; +import { MAX_TOP_K, MIN_TOP_K } from '../../../constants.js'; import { getPineconeClient } from '../client-context.js'; import { formatQueryResultRows } from '../format-query-result.js'; import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js'; @@ -109,7 +109,7 @@ export function registerKeywordSearchTool(server: McpServer): void { 'keyword_search', { description: - 'Keyword (lexical/sparse-only) search over the Pinecone sparse index (default: rag-hybrid-sparse). ' + + 'Keyword (lexical/sparse-only) search over the Pinecone sparse index ({indexName}-sparse). ' + 'Use for exact or keyword-style queries. Does not use semantic reranking. ' + 'Call list_namespaces first to discover namespaces. Does not require suggest_query_params.', inputSchema: { diff --git a/src/server/tools/list-namespaces-tool.test.ts b/src/core/server/tools/list-namespaces-tool.test.ts similarity index 100% rename from src/server/tools/list-namespaces-tool.test.ts rename to src/core/server/tools/list-namespaces-tool.test.ts diff --git a/src/server/tools/list-namespaces-tool.ts b/src/core/server/tools/list-namespaces-tool.ts similarity index 100% rename from src/server/tools/list-namespaces-tool.ts rename to src/core/server/tools/list-namespaces-tool.ts diff --git a/src/server/tools/namespace-router-tool.test.ts b/src/core/server/tools/namespace-router-tool.test.ts similarity index 100% rename from src/server/tools/namespace-router-tool.test.ts rename to src/core/server/tools/namespace-router-tool.test.ts diff --git a/src/server/tools/namespace-router-tool.ts b/src/core/server/tools/namespace-router-tool.ts similarity index 100% rename from src/server/tools/namespace-router-tool.ts rename to src/core/server/tools/namespace-router-tool.ts diff --git a/src/server/tools/query-documents-tool.test.ts b/src/core/server/tools/query-documents-tool.test.ts similarity index 99% rename from src/server/tools/query-documents-tool.test.ts rename to src/core/server/tools/query-documents-tool.test.ts index 1d0a713..2affd46 100644 --- a/src/server/tools/query-documents-tool.test.ts +++ b/src/core/server/tools/query-documents-tool.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_QUERY_DOCUMENTS_TOP_K, QUERY_DOCUMENTS_MAX_CHUNKS } from '../../constants.js'; +import { DEFAULT_QUERY_DOCUMENTS_TOP_K, QUERY_DOCUMENTS_MAX_CHUNKS } from '../../../constants.js'; import { getPineconeClient } from '../client-context.js'; import { reassembleByDocument } from '../reassemble-documents.js'; import * as suggestionFlow from '../suggestion-flow.js'; diff --git a/src/server/tools/query-documents-tool.ts b/src/core/server/tools/query-documents-tool.ts similarity index 97% rename from src/server/tools/query-documents-tool.ts rename to src/core/server/tools/query-documents-tool.ts index adc1c69..b115ecc 100644 --- a/src/server/tools/query-documents-tool.ts +++ b/src/core/server/tools/query-documents-tool.ts @@ -4,7 +4,7 @@ import { DEFAULT_QUERY_DOCUMENTS_TOP_K, MAX_QUERY_DOCUMENTS_TOP_K, QUERY_DOCUMENTS_MAX_CHUNKS, -} from '../../constants.js'; +} from '../../../constants.js'; import { getPineconeClient } from '../client-context.js'; import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js'; import { normalizeNamespace } from '../namespace-utils.js'; @@ -37,7 +37,7 @@ export function registerQueryDocumentsTool(server: McpServer): void { { description: 'Run a semantic query and return whole documents (reassembled from chunks). ' + - 'Always uses semantic reranking for document-level relevance (higher latency/cost than chunk-only query). ' + + 'Reranks for document-level relevance when a rerank model is configured (higher latency/cost than chunk-only query). ' + 'Use for content analysis, summarization, or when you need full-document context. ' + 'Chunks are grouped by document_number/doc_id/url, ordered by chunk_index when present (e.g. from RecursiveCharacterTextSplitter), and merged into one content per document. ' + 'Requires suggest_query_params to be called first for the target namespace. Use list_namespaces to discover namespaces.', diff --git a/src/server/tools/query-tool.test.ts b/src/core/server/tools/query-tool.test.ts similarity index 99% rename from src/server/tools/query-tool.test.ts rename to src/core/server/tools/query-tool.test.ts index 167199d..ae8ca21 100644 --- a/src/server/tools/query-tool.test.ts +++ b/src/core/server/tools/query-tool.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { FAST_QUERY_FIELDS } from '../../constants.js'; +import { FAST_QUERY_FIELDS } from '../../../constants.js'; import { getPineconeClient } from '../client-context.js'; import * as suggestionFlow from '../suggestion-flow.js'; import { registerQueryTool } from './query-tool.js'; diff --git a/src/server/tools/query-tool.ts b/src/core/server/tools/query-tool.ts similarity index 97% rename from src/server/tools/query-tool.ts rename to src/core/server/tools/query-tool.ts index ddbbc77..e19ba87 100644 --- a/src/server/tools/query-tool.ts +++ b/src/core/server/tools/query-tool.ts @@ -1,7 +1,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; -import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../constants.js'; -import type { QueryResponse } from '../../types.js'; +import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../../constants.js'; +import type { QueryResponse } from '../../../types.js'; import { getPineconeClient } from '../client-context.js'; import { formatQueryResultRows } from '../format-query-result.js'; import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js'; diff --git a/src/server/tools/test-helpers.ts b/src/core/server/tools/test-helpers.ts similarity index 94% rename from src/server/tools/test-helpers.ts rename to src/core/server/tools/test-helpers.ts index b0e4541..fa5a843 100644 --- a/src/server/tools/test-helpers.ts +++ b/src/core/server/tools/test-helpers.ts @@ -1,4 +1,4 @@ -import type { HybridQueryResult, SearchResult } from '../../types.js'; +import type { HybridQueryResult, SearchResult } from '../../../types.js'; import type { ToolError, ToolErrorCode } from '../tool-error.js'; import { toolErrorSchema } from '../tool-error.js'; @@ -85,6 +85,9 @@ export function makeHybridQueryResult(overrides?: Partial): H ? { degradation_reason: overrides.degradation_reason } : {}), hybrid_leg_failed: overrides?.hybrid_leg_failed ?? null, + ...(overrides?.rerank_skipped_reason !== undefined + ? { rerank_skipped_reason: overrides.rerank_skipped_reason } + : {}), }; } diff --git a/src/core/server/url-registry.test.ts b/src/core/server/url-registry.test.ts new file mode 100644 index 0000000..acf1713 --- /dev/null +++ b/src/core/server/url-registry.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, afterEach } from 'vitest'; +import type { UrlGeneratorFn } from './url-registry.js'; +import { + generateUrlForNamespace, + registerUrlGenerator, + unregisterUrlGenerator, + resetUrlGenerationRegistry, +} from './url-registry.js'; + +describe('url-registry', () => { + afterEach(() => { + resetUrlGenerationRegistry(); + }); + + it('uses existing metadata.url when present', () => { + registerUrlGenerator('custom', () => ({ + url: 'https://override.example', + method: 'generated.custom', + })); + const r = generateUrlForNamespace('custom', { + url: 'https://example.com/custom', + doc_id: 'ignored', + }); + expect(r.url).toBe('https://example.com/custom'); + expect(r.method).toBe('metadata.url'); + }); + + it('returns unavailable for unsupported namespace', () => { + const r = generateUrlForNamespace('unknown-ns', { doc_id: 'x' }); + expect(r.url).toBeNull(); + expect(r.method).toBe('unavailable'); + }); +}); + +describe('registerUrlGenerator', () => { + const customNs = 'acme-docs'; + + afterEach(() => { + unregisterUrlGenerator(customNs); + resetUrlGenerationRegistry(); + }); + + it('registers a custom generator for a new namespace', () => { + const fn: UrlGeneratorFn = () => ({ + url: 'https://example.com/doc/1', + method: 'generated.custom', + }); + registerUrlGenerator(customNs, fn); + const r = generateUrlForNamespace(customNs, {}); + expect(r.url).toBe('https://example.com/doc/1'); + expect(r.method).toBe('generated.custom'); + }); +}); diff --git a/src/core/server/url-registry.ts b/src/core/server/url-registry.ts new file mode 100644 index 0000000..bc5efe2 --- /dev/null +++ b/src/core/server/url-registry.ts @@ -0,0 +1,102 @@ +/** + * Per-namespace URL generation registry (generic API). + * + * Domain-specific built-in generators live in `src/alliance/url-builtins.ts`. + * Library consumers can plug in their own with `registerUrlGenerator(namespace, generator)`. + */ + +/** Outcome of a URL-generation attempt. */ +export type UrlGenerationResult = { + url: string | null; + method: + | 'metadata.url' + | 'metadata.source' + | 'generated.mailing' + | 'generated.slack' + | 'generated.custom' + | 'unavailable'; + reason?: string; +}; + +/** + * Function that builds a URL for a record's metadata. + * + * Custom generators may return any of the standard `method` values, plus + * `'generated.custom'` for namespace-specific generators registered by + * library consumers. + */ +export type UrlGenerator = (metadata: Record) => UrlGenerationResult; + +/** + * Alias for {@link UrlGenerator} (issue / API naming: `UrlGeneratorFn`). + * Use either type when implementing custom URL synthesis. + */ +export type UrlGeneratorFn = UrlGenerator; + +/** Registry of namespace -> URL generator. */ +const urlGenerators = new Map(); + +/** Return a trimmed non-empty string or null for empty/missing values. */ +function asString(value: unknown): string | null { + return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; +} + +/** + * Clear all URL generators. + * Used by {@link teardownServer} so a subsequent setup can reinstall generators. + */ +export function resetUrlGenerationRegistry(): void { + urlGenerators.clear(); +} + +/** + * Register a URL generator for a namespace, replacing any existing entry. + * + * @param namespace exact namespace name (matches the value returned by `list_namespaces`). + * @param generator function that turns a record's metadata into a URL ({@link UrlGeneratorFn}). + */ +export function registerUrlGenerator(namespace: string, generator: UrlGeneratorFn): void { + const normalizedNamespace = namespace.trim(); + if (normalizedNamespace.length === 0) { + throw new TypeError('namespace must be a non-empty string'); + } + if (typeof generator !== 'function') { + throw new TypeError('generator must be a function'); + } + urlGenerators.set(normalizedNamespace, generator); +} + +/** Remove a namespace's URL generator. Returns true if a generator was removed. */ +export function unregisterUrlGenerator(namespace: string): boolean { + return urlGenerators.delete(namespace); +} + +/** True when the namespace has a registered URL generator (does not consider `metadata.url`). */ +export function hasUrlGenerator(namespace: string): boolean { + return urlGenerators.has(namespace); +} + +/** + * Generate a URL for a record in the given namespace when metadata.url is missing. + * Uses the registry of URL generators; returns unavailable for namespaces without a generator. + */ +export function generateUrlForNamespace( + namespace: string, + metadata: Record +): UrlGenerationResult { + const existingUrl = asString(metadata['url']); + if (existingUrl) { + return { url: existingUrl, method: 'metadata.url' }; + } + + const generator = urlGenerators.get(namespace); + if (generator) { + return generator(metadata); + } + + return { + url: null, + method: 'unavailable', + reason: `URL generation is not supported for namespace "${namespace}"`, + }; +} diff --git a/src/core/setup.ts b/src/core/setup.ts new file mode 100644 index 0000000..ecfc337 --- /dev/null +++ b/src/core/setup.ts @@ -0,0 +1,77 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION } from '../constants.js'; +import { warn as logWarn } from '../logger.js'; +import type { ServerConfig } from './config.js'; +import { clearPineconeClient } from './server/client-context.js'; +import { setServerConfig, resetServerConfig } from './server/config-context.js'; +import { invalidateNamespacesCache } from './server/namespaces-cache.js'; +import { resetSuggestionFlow } from './server/suggestion-flow.js'; +import { resetUrlGenerationRegistry } from './server/url-registry.js'; +import { registerCountTool } from './server/tools/count-tool.js'; +import { registerGenerateUrlsTool } from './server/tools/generate-urls-tool.js'; +import { registerKeywordSearchTool } from './server/tools/keyword-search-tool.js'; +import { registerListNamespacesTool } from './server/tools/list-namespaces-tool.js'; +import { registerNamespaceRouterTool } from './server/tools/namespace-router-tool.js'; +import { registerQueryDocumentsTool } from './server/tools/query-documents-tool.js'; +import { registerQueryTool } from './server/tools/query-tool.js'; + +let mcpServerInitialized = false; + +/** + * Reset process-global MCP server state (suggest-flow, namespace cache, active config, + * Pinecone client handle, URL generator registry). Call before a second {@link setupCoreServer}. + */ +export function teardownServer(): void { + resetSuggestionFlow(); + invalidateNamespacesCache(); + resetServerConfig(); + clearPineconeClient(); + resetUrlGenerationRegistry(); + mcpServerInitialized = false; +} + +/** + * Create and configure the MCP server with generic (core) tools only. + * + * Does not register Alliance-specific tools (`suggest_query_params`, `guided_query`) + * or built-in Boost/Slack URL generators. Use {@link setupAllianceServer} from + * `@will-cppa/pinecone-read-only-mcp/alliance` for the full tool surface. + */ +export async function setupCoreServer(config?: ServerConfig): Promise { + if (mcpServerInitialized) { + throw new Error( + 'setupCoreServer() already called in this process. The MCP server uses process-global state (suggest-flow, namespace cache, URL generators, config). Call teardownServer() first if you need to re-initialize.' + ); + } + + if (config) { + setServerConfig(config); + if (!config.rerankModel) { + logWarn( + 'Reranking disabled: set PINECONE_RERANK_MODEL or pass rerankModel. ' + + 'Alliance deployments: use resolveAllianceConfig / setupAllianceServer for the default rerank model.' + ); + } + } + + const server = new McpServer( + { + name: SERVER_NAME, + version: SERVER_VERSION, + }, + { + instructions: SERVER_INSTRUCTIONS, + } + ); + + registerListNamespacesTool(server); + registerNamespaceRouterTool(server); + registerCountTool(server); + registerQueryTool(server); + registerKeywordSearchTool(server); + registerQueryDocumentsTool(server); + registerGenerateUrlsTool(server); + + mcpServerInitialized = true; + return server; +} diff --git a/src/index.ts b/src/index.ts index 1050743..67610d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,24 +3,25 @@ /** * Pinecone Read-Only MCP CLI entry point. * - * Thin composition root: parseCli() -> resolveConfig() -> setupServer(config) - * -> connect to stdio transport. All argv parsing lives in `src/cli.ts`; - * all configuration/defaults live in `src/config.ts`. + * Thin composition root: parseCli() -> resolveAllianceConfig() -> setupAllianceServer(config) + * -> connect to stdio transport. */ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import * as dotenv from 'dotenv'; import { parseCli, printHelp, printVersion } from './cli.js'; -import { resolveConfig, type ServerConfig } from './config.js'; -import { PineconeClient } from './pinecone-client.js'; -import { setupServer, setPineconeClient } from './server.js'; +import { resolveAllianceConfig } from './alliance/config.js'; +import type { ServerConfig } from './core/config.js'; +import { PineconeClient } from './core/pinecone-client.js'; +import { setPineconeClient } from './core/server/client-context.js'; +import { setupAllianceServer } from './alliance/setup.js'; import { setLogFormat, setLogLevel, warn as logWarn } from './logger.js'; dotenv.config(); /** * Build a config from CLI argv + environment, exiting fast on - * --help, --version, or missing API key. + * --help, --version, or missing API key / index name. */ function buildConfigOrExit(): ServerConfig { const parsed = parseCli(); @@ -34,7 +35,7 @@ function buildConfigOrExit(): ServerConfig { } try { - return resolveConfig(parsed.overrides); + return resolveAllianceConfig(parsed.overrides); } catch (err) { const message = err instanceof Error ? err.message : String(err); process.stderr.write(`Error: ${message}\n`); @@ -82,9 +83,12 @@ async function main(): Promise { process.stderr.write( `Using Pinecone index: ${config.indexName} (sparse: ${config.sparseIndexName})\n` ); + if (config.rerankModel) { + process.stderr.write(`Rerank model: ${config.rerankModel}\n`); + } process.stderr.write(`Log level: ${config.logLevel} (format: ${config.logFormat})\n`); - const server = await setupServer(config); + const server = await setupAllianceServer(config); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/logger.ts b/src/logger.ts index 5c829ac..1baf5d0 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -11,7 +11,7 @@ * cannot leak through `error()`. */ -import type { LogFormat, LogLevel } from './config.js'; +import type { LogFormat, LogLevel } from './core/config.js'; const LEVEL_ORDER: Record = { DEBUG: 0, diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 05fcad2..0000000 --- a/src/server.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @packageDocumentation - * **@will-cppa/pinecone-read-only-mcp** — programmatic entrypoint for the - * Pinecone read-only MCP server. - * - * Import from the package root: - * - * - {@link setupServer} — build an `McpServer` with all tools registered (at most once per process unless {@link teardownServer} runs). - * - {@link teardownServer} — clear process-global server state so {@link setupServer} can run again (tests, re-embedding). - * - {@link PineconeClient} — hybrid search, count, namespace listing, etc. - * - {@link resolveConfig} — merge CLI-style overrides with `process.env`. - * - {@link setPineconeClient} — inject a client instance before `setupServer()`. - * - {@link registerUrlGenerator} / {@link unregisterUrlGenerator} — extend URL synthesis (`UrlGeneratorFn`). - * - {@link toolErrorSchema} / {@link ToolError} — parse MCP tool failures (`isError: true` JSON bodies). - * - Built-in `mailing` / `slack-Cpplang` URL generators are registered from {@link setupServer} - * via {@link registerBuiltinUrlGenerators}; call it yourself if you use the library without `setupServer`. - * - * The CLI binary (`pinecone-read-only-mcp`) lives in `dist/index.js` and is not - * exported from this module. - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION } from './constants.js'; -import type { ServerConfig } from './config.js'; -import { setServerConfig, resetServerConfig } from './server/config-context.js'; -import { clearPineconeClient } from './server/client-context.js'; -import { - registerBuiltinUrlGenerators, - resetUrlGenerationRegistry, -} from './server/url-generation.js'; -import { invalidateNamespacesCache } from './server/namespaces-cache.js'; -import { resetSuggestionFlow } from './server/suggestion-flow.js'; -import { registerCountTool } from './server/tools/count-tool.js'; -import { registerGuidedQueryTool } from './server/tools/guided-query-tool.js'; -import { registerGenerateUrlsTool } from './server/tools/generate-urls-tool.js'; -import { registerKeywordSearchTool } from './server/tools/keyword-search-tool.js'; -import { registerListNamespacesTool } from './server/tools/list-namespaces-tool.js'; -import { registerNamespaceRouterTool } from './server/tools/namespace-router-tool.js'; -import { registerQueryDocumentsTool } from './server/tools/query-documents-tool.js'; -import { registerQueryTool } from './server/tools/query-tool.js'; -import { registerSuggestQueryParamsTool } from './server/tools/suggest-query-params-tool.js'; - -export { setPineconeClient } from './server/client-context.js'; -/** Validate user-supplied Pinecone metadata filter objects before querying. */ -export { validateMetadataFilter } from './server/metadata-filter.js'; -/** Structured metadata filter validation (`field` dot-path); {@link validateMetadataFilter} remains a string-only wrapper. */ -export { validateMetadataFilterDetailed } from './server/metadata-filter.js'; -export type { MetadataFilterValidationError } from './server/metadata-filter.js'; -/** Zod schema and types for MCP tool error JSON bodies (`isError: true`). */ -export { toolErrorSchema } from './server/tool-error.js'; -export type { ToolError, ToolErrorCode } from './server/tool-error.js'; -/** Heuristic field + tool suggestions from a namespace schema + user query. */ -export { suggestQueryParams } from './server/query-suggestion.js'; -export type { RecommendedTool, SuggestQueryParamsResult } from './server/query-suggestion.js'; -/** Register custom per-namespace URL synthesis used by `generate_urls` / row enrichment. */ -export { - registerUrlGenerator, - unregisterUrlGenerator, - generateUrlForNamespace, - hasUrlGenerator, - registerBuiltinUrlGenerators, -} from './server/url-generation.js'; -export type { - UrlGenerationResult, - UrlGenerator, - UrlGeneratorFn, - RegisterBuiltinUrlGeneratorsOptions, -} from './server/url-generation.js'; -/** Build {@link ServerConfig} from CLI overrides + environment variables. */ -export { resolveConfig } from './config.js'; -export type { ServerConfig, LogLevel, LogFormat, ConfigOverrides } from './config.js'; -/** Pinecone SDK wrapper: hybrid query, keyword search, count, namespace metadata. */ -export { PineconeClient } from './pinecone-client.js'; -export type { - PineconeClientConfig, - QueryParams, - CountParams, - CountResult, - KeywordSearchParams, - SearchResult, - PineconeMetadataValue, - QueryResponse, - QueryResultRowShape, - KeywordIndexNamespacesResult, - HybridQueryResult, - HybridLegFailed, -} from './types.js'; - -let mcpServerInitialized = false; - -/** - * Reset process-global MCP server state (suggest-flow, namespace cache, active config, - * Pinecone client handle, URL generator registry). Call before a second {@link setupServer}. - */ -export function teardownServer(): void { - resetSuggestionFlow(); - invalidateNamespacesCache(); - resetServerConfig(); - clearPineconeClient(); - resetUrlGenerationRegistry(); - mcpServerInitialized = false; -} - -/** - * Create and configure the MCP server with all tools. - * - * Process-global state (one MCP client per Node process is assumed): - * suggest-flow gate (`stateByNamespace`), namespaces cache, URL generator registry, - * and {@link setServerConfig} — see README “Deployment model”. Multi-tenant HTTP - * multiplexing can violate the suggest-flow guarantee unless you isolate by session. - * - * A second call in the same process throws unless {@link teardownServer} runs first. - * - * @returns the configured `McpServer` instance, ready to connect to a transport. - */ -export async function setupServer(config?: ServerConfig): Promise { - if (mcpServerInitialized) { - throw new Error( - 'setupServer() already called in this process. The MCP server uses process-global state (suggest-flow, namespace cache, URL generators, config). Call teardownServer() first if you need to re-initialize.' - ); - } - - if (config) { - setServerConfig(config); - } - - registerBuiltinUrlGenerators(); - - const server = new McpServer( - { - name: SERVER_NAME, - version: SERVER_VERSION, - }, - { - instructions: SERVER_INSTRUCTIONS, - } - ); - - registerListNamespacesTool(server); - registerNamespaceRouterTool(server); - registerSuggestQueryParamsTool(server); - registerCountTool(server); - registerQueryTool(server); - registerKeywordSearchTool(server); - registerQueryDocumentsTool(server); - registerGuidedQueryTool(server); - registerGenerateUrlsTool(server); - - mcpServerInitialized = true; - return server; -} diff --git a/src/server/url-generation.test.ts b/src/server/url-generation.test.ts deleted file mode 100644 index c6269f9..0000000 --- a/src/server/url-generation.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, expect, it, beforeAll, afterEach } from 'vitest'; -import type { UrlGeneratorFn } from './url-generation.js'; -import { - generateUrlForNamespace, - registerBuiltinUrlGenerators, - registerUrlGenerator, - unregisterUrlGenerator, -} from './url-generation.js'; - -beforeAll(() => { - registerBuiltinUrlGenerators(); -}); - -describe('generateUrlForNamespace', () => { - it('uses existing metadata.url when present', () => { - const r = generateUrlForNamespace('mailing', { - url: 'https://example.com/custom', - doc_id: 'ignored', - }); - expect(r.url).toBe('https://example.com/custom'); - expect(r.method).toBe('metadata.url'); - }); - - it('generates mailing URL from doc_id', () => { - const r = generateUrlForNamespace('mailing', { - doc_id: 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6', - }); - expect(r.url).toBe( - 'https://lists.boost.org/archives/list/boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6/' - ); - expect(r.method).toBe('generated.mailing'); - }); - - it('generates mailing URL from thread_id when doc_id missing', () => { - const r = generateUrlForNamespace('mailing', { - thread_id: 'boost@lists.boost.org/thread/ABC123', - }); - expect(r.url).toBe( - 'https://lists.boost.org/archives/list/boost@lists.boost.org/thread/ABC123/' - ); - expect(r.method).toBe('generated.mailing'); - }); - - it('generates mailing URL as list_name/message/doc_id when list_name present and doc_id does not contain it', () => { - const r = generateUrlForNamespace('mailing', { - list_name: 'boost-users', - doc_id: '12345', - }); - expect(r.url).toBe('https://lists.boost.org/archives/list/boost-users/message/12345/'); - expect(r.method).toBe('generated.mailing'); - }); - - it('uses msg_id when list_name present and doc_id missing', () => { - const r = generateUrlForNamespace('mailing', { - list_name: 'boost-announce', - msg_id: '67890', - }); - expect(r.url).toBe('https://lists.boost.org/archives/list/boost-announce/message/67890/'); - expect(r.method).toBe('generated.mailing'); - }); - - it('uses single-path form when doc_id contains list_name (no list_name/message split)', () => { - const r = generateUrlForNamespace('mailing', { - list_name: 'boost-users', - doc_id: 'boost-users@lists.boost.org/message/12345', - }); - expect(r.url).toBe( - 'https://lists.boost.org/archives/list/boost-users@lists.boost.org/message/12345/' - ); - expect(r.method).toBe('generated.mailing'); - }); - - it('uses slack source when available', () => { - const r = generateUrlForNamespace('slack-Cpplang', { - source: 'https://app.slack.com/client/T123/C123/p123', - team_id: 'T999', - channel_id: 'C999', - doc_id: '1.2', - }); - expect(r.url).toBe('https://app.slack.com/client/T123/C123/p123'); - expect(r.method).toBe('metadata.source'); - }); - - it('generates slack URL from team/channel/doc_id', () => { - const r = generateUrlForNamespace('slack-Cpplang', { - team_id: 'T123456789', - channel_id: 'C123456', - doc_id: '1234567.890', - }); - expect(r.url).toBe('https://app.slack.com/client/T123456789/C123456/p1234567890'); - expect(r.method).toBe('generated.slack'); - }); - - it('returns unavailable for unsupported namespace', () => { - const r = generateUrlForNamespace('wg21-papers', { doc_id: 'x' }); - expect(r.url).toBeNull(); - expect(r.method).toBe('unavailable'); - }); - it('returns unavailable for mailing when no doc_id or thread_id', () => { - const r = generateUrlForNamespace('mailing', { author: 'someone' }); - expect(r.url).toBeNull(); - expect(r.method).toBe('unavailable'); - }); - - it('returns unavailable for slack-Cpplang when required fields are missing', () => { - const r = generateUrlForNamespace('slack-Cpplang', { - team_id: 'T123', - // channel_id missing, doc_id missing, no source - }); - expect(r.url).toBeNull(); - expect(r.method).toBe('unavailable'); - }); -}); - -describe('registerUrlGenerator', () => { - const customNs = 'acme-docs'; - - afterEach(() => { - unregisterUrlGenerator(customNs); - registerBuiltinUrlGenerators({ reinstallBuiltins: true }); - }); - - it('registers a custom generator for a new namespace', () => { - const fn: UrlGeneratorFn = () => ({ - url: 'https://example.com/doc/1', - method: 'generated.custom', - }); - registerUrlGenerator(customNs, fn); - const r = generateUrlForNamespace(customNs, {}); - expect(r.url).toBe('https://example.com/doc/1'); - expect(r.method).toBe('generated.custom'); - }); - - it('allows a custom generator to override the mailing built-in', () => { - registerUrlGenerator('mailing', () => ({ - url: 'https://override.example/mailing', - method: 'generated.custom', - })); - const r = generateUrlForNamespace('mailing', { - doc_id: 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6', - }); - expect(r.url).toBe('https://override.example/mailing'); - expect(r.method).toBe('generated.custom'); - }); -}); diff --git a/src/server/url-generation.ts b/src/server/url-generation.ts deleted file mode 100644 index e64c4c9..0000000 --- a/src/server/url-generation.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Per-namespace URL generation registry. - * - * Built-in generators cover `mailing` (Boost archive style) and - * `slack-Cpplang`. Library consumers can plug in their own with - * `registerUrlGenerator(namespace, generator)`. - */ - -/** Outcome of a URL-generation attempt. */ -export type UrlGenerationResult = { - url: string | null; - method: - | 'metadata.url' - | 'metadata.source' - | 'generated.mailing' - | 'generated.slack' - | 'generated.custom' - | 'unavailable'; - reason?: string; -}; - -/** - * Function that builds a URL for a record's metadata. - * - * Custom generators may return any of the standard `method` values, plus - * `'generated.custom'` for namespace-specific generators registered by - * library consumers. - */ -export type UrlGenerator = (metadata: Record) => UrlGenerationResult; - -/** - * Alias for {@link UrlGenerator} (issue / API naming: `UrlGeneratorFn`). - * Use either type when implementing custom URL synthesis. - */ -export type UrlGeneratorFn = UrlGenerator; - -/** Registry of namespace -> URL generator. Built-ins register via {@link registerBuiltinUrlGenerators}. */ -const urlGenerators = new Map(); - -/** Return a trimmed non-empty string or null for empty/missing values. */ -function asString(value: unknown): string | null { - return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; -} - -/** - * Build a mailing-list URL (e.g. Boost archives). - * Two cases: - * 1. If metadata has list_name and doc_id (or msg_id) and the message id does not contain list_name, - * URL is https://lists.boost.org/archives/list/{list_name}/message/{doc_id}/ - * 2. Otherwise use doc_id or thread_id as the list path: .../list/{doc_id_or_thread_id}/ - */ -function generatorMailing(metadata: Record): UrlGenerationResult { - const listName = asString(metadata['list_name']); - const docId = asString(metadata['doc_id']) ?? asString(metadata['msg_id']); - const threadId = asString(metadata['thread_id']); - - if (listName && docId && !docId.includes(listName)) { - return { - url: `https://lists.boost.org/archives/list/${listName}/message/${docId}/`, - method: 'generated.mailing', - }; - } - - const docIdOrThread = docId ?? threadId; - if (!docIdOrThread) { - return { - url: null, - method: 'unavailable', - reason: 'mailing requires doc_id, msg_id, or thread_id to generate URL', - }; - } - return { - url: `https://lists.boost.org/archives/list/${docIdOrThread}/`, - method: 'generated.mailing', - }; -} - -/** Build a Slack message URL from source or team_id/channel_id/doc_id. */ -function generatorSlackCpplang(metadata: Record): UrlGenerationResult { - const source = asString(metadata['source']); - if (source) { - return { url: source, method: 'metadata.source' }; - } - const teamId = asString(metadata['team_id']); - const channelId = asString(metadata['channel_id']); - const docId = asString(metadata['doc_id']); - if (!teamId || !channelId || !docId) { - return { - url: null, - method: 'unavailable', - reason: 'slack-Cpplang requires team_id, channel_id, and doc_id (or source)', - }; - } - const messageId = docId.replace(/\./g, ''); - return { - url: `https://app.slack.com/client/${teamId}/${channelId}/p${messageId}`, - method: 'generated.slack', - }; -} - -let builtinGeneratorsRegistered = false; - -/** Options for {@link registerBuiltinUrlGenerators}. */ -export type RegisterBuiltinUrlGeneratorsOptions = { - /** - * When `true`, re-applies the built-in `mailing` and `slack-Cpplang` generators - * even if they were already registered or were replaced by {@link registerUrlGenerator}. - * Use to restore defaults after an override (e.g. in tests). - */ - reinstallBuiltins?: boolean; -}; - -/** - * Register built-in generators (`mailing`, `slack-Cpplang`). - * - * With no options (or `reinstallBuiltins` omitted / `false`), the call is idempotent: - * only the first invocation in the process installs the two built-ins. - * - * With `{ reinstallBuiltins: true }`, always resets `mailing` and `slack-Cpplang` to - * the library implementations (does not remove other custom namespaces). - * - * Invoked from {@link setupServer} so embedders get the same defaults as the CLI; - * pure library use without calling `setupServer` should register explicitly if needed. - */ -export function registerBuiltinUrlGenerators(options?: RegisterBuiltinUrlGeneratorsOptions): void { - if (options?.reinstallBuiltins) { - urlGenerators.set('mailing', generatorMailing); - urlGenerators.set('slack-Cpplang', generatorSlackCpplang); - builtinGeneratorsRegistered = true; - return; - } - if (builtinGeneratorsRegistered) return; - urlGenerators.set('mailing', generatorMailing); - urlGenerators.set('slack-Cpplang', generatorSlackCpplang); - builtinGeneratorsRegistered = true; -} - -/** - * Clear all URL generators and the built-in registration latch. - * Used by {@link teardownServer} so a subsequent `setupServer()` can reinstall built-ins. - */ -export function resetUrlGenerationRegistry(): void { - urlGenerators.clear(); - builtinGeneratorsRegistered = false; -} - -/** - * Register a URL generator for a namespace, replacing any existing entry. - * - * @param namespace exact namespace name (matches the value returned by `list_namespaces`). - * @param generator function that turns a record's metadata into a URL ({@link UrlGeneratorFn}). - * - * @example - * ```ts - * import { registerUrlGenerator } from '@will-cppa/pinecone-read-only-mcp'; - * - * registerUrlGenerator('my-docs', (metadata) => { - * const id = typeof metadata.doc_id === 'string' ? metadata.doc_id : null; - * return id - * ? { url: `https://docs.example.com/${id}`, method: 'generated.custom' } - * : { url: null, method: 'unavailable', reason: 'doc_id missing' }; - * }); - * ``` - */ -export function registerUrlGenerator(namespace: string, generator: UrlGeneratorFn): void { - const normalizedNamespace = namespace.trim(); - if (normalizedNamespace.length === 0) { - throw new TypeError('namespace must be a non-empty string'); - } - if (typeof generator !== 'function') { - throw new TypeError('generator must be a function'); - } - urlGenerators.set(normalizedNamespace, generator); -} - -/** Remove a namespace's URL generator. Returns true if a generator was removed. */ -export function unregisterUrlGenerator(namespace: string): boolean { - return urlGenerators.delete(namespace); -} - -/** True when the namespace has a registered URL generator (does not consider `metadata.url`). */ -export function hasUrlGenerator(namespace: string): boolean { - return urlGenerators.has(namespace); -} - -/** - * Generate a URL for a record in the given namespace when metadata.url is missing. - * Uses the registry of URL generators; returns unavailable for namespaces without a generator. - */ -export function generateUrlForNamespace( - namespace: string, - metadata: Record -): UrlGenerationResult { - const existingUrl = asString(metadata['url']); - if (existingUrl) { - return { url: existingUrl, method: 'metadata.url' }; - } - - const generator = urlGenerators.get(namespace); - if (generator) { - return generator(metadata); - } - - return { - url: null, - method: 'unavailable', - reason: `URL generation is not supported for namespace "${namespace}"`, - }; -} diff --git a/src/types.ts b/src/types.ts index b99e7c2..74066f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,18 +8,17 @@ export type PineconeMetadataValue = string | number | boolean | string[]; /** * Configuration for `new PineconeClient(config)`. * - * `apiKey` is required. All other fields are optional and fall back to - * sensible defaults from `src/constants.ts`. Values are expected to come from + * `apiKey` and `indexName` are required. Values are expected to come from * {@link resolveConfig} / CLI or an equivalent resolved object — `PineconeClient` * does not read `process.env` directly. */ export interface PineconeClientConfig { apiKey: string; - /** Dense (hybrid) index name. */ - indexName?: string; + /** Dense (hybrid) index name. Required. */ + indexName: string; /** Sparse index name. Defaults to `${indexName}-sparse`. */ sparseIndexName?: string; - /** Reranker model identifier. */ + /** Reranker model identifier. When unset, reranking is disabled. */ rerankModel?: string; /** Default top-k for `query()`. */ defaultTopK?: number; @@ -41,6 +40,9 @@ export type HybridLegFailed = 'dense' | 'sparse' | null; /** * Outcome of {@link PineconeClient.query}: result rows plus degradation signals for MCP clients. */ +/** Why semantic rerank was not applied despite `useReranking: true`. */ +export type RerankSkippedReason = 'no_model'; + export interface HybridQueryResult { results: SearchResult[]; /** True when reranking was attempted and failed (rows may have `reranked: false`). */ @@ -49,6 +51,11 @@ export interface HybridQueryResult { degradation_reason?: string; /** Set when exactly one of dense/sparse search failed but the other succeeded. */ hybrid_leg_failed: HybridLegFailed; + /** + * Set when `useReranking` was true but no rerank model is configured on the client + * (generic core embedders). Alliance CLI uses {@link resolveAllianceConfig} to avoid this. + */ + rerank_skipped_reason?: RerankSkippedReason; } export interface PineconeHit { From 108d06234652f121aa717f750787909fa7314507 Mon Sep 17 00:00:00 2001 From: zho Date: Tue, 26 May 2026 02:15:27 +0800 Subject: [PATCH 2/6] fixed format check results --- src/alliance/config.ts | 6 +----- src/core/rerank-trace.test.ts | 5 +---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/alliance/config.ts b/src/alliance/config.ts index 47aacd4..f891afa 100644 --- a/src/alliance/config.ts +++ b/src/alliance/config.ts @@ -3,11 +3,7 @@ * that must not live in the generic core package. */ -import { - resolveConfig, - type ConfigOverrides, - type ServerConfig, -} from '../core/config.js'; +import { resolveConfig, type ConfigOverrides, type ServerConfig } from '../core/config.js'; /** Default Pinecone inference rerank model for C++ Alliance deployments (CLI / alliance entry). */ export const DEFAULT_ALLIANCE_RERANK_MODEL = 'bge-reranker-v2-m3'; diff --git a/src/core/rerank-trace.test.ts b/src/core/rerank-trace.test.ts index 1233c8a..1eb785a 100644 --- a/src/core/rerank-trace.test.ts +++ b/src/core/rerank-trace.test.ts @@ -9,10 +9,7 @@ describe('guidedRerankStatus', () => { it('returns skipped_no_model when outcome reports no rerank model', () => { expect( - guidedRerankStatus( - true, - makeHybridQueryResult({ rerank_skipped_reason: 'no_model' }) - ) + guidedRerankStatus(true, makeHybridQueryResult({ rerank_skipped_reason: 'no_model' })) ).toBe('skipped_no_model'); }); From 5dbfd2df6407a594748f7e188b9c3db84cd48e89 Mon Sep 17 00:00:00 2001 From: zho Date: Tue, 26 May 2026 03:55:17 +0800 Subject: [PATCH 3/6] updated to use default rerank model --- .env.example | 3 +-- CHANGELOG.md | 4 ++-- README.md | 30 ++++++++++++++------------ docs/CONFIGURATION.md | 10 ++++----- examples/README.md | 12 +++++------ examples/custom-url-generator.ts | 16 ++++++++------ examples/guided-query-demo.ts | 11 +++++----- examples/library-embedding-demo.ts | 13 ++++++------ examples/suggest-flow-demo.ts | 11 +++++----- src/alliance/config.test.ts | 28 +++--------------------- src/alliance/config.ts | 34 ++++++++++++------------------ src/alliance/setup.ts | 7 ++---- src/cli.ts | 2 +- src/core/config.test.ts | 18 +++++++++++++--- src/core/config.ts | 12 +++++++---- src/core/index.ts | 2 +- src/core/pinecone-client.ts | 3 ++- src/core/server/url-registry.ts | 6 +++--- src/core/setup.ts | 7 ------ src/index.ts | 7 +++--- 20 files changed, 110 insertions(+), 126 deletions(-) diff --git a/.env.example b/.env.example index be94f69..4ac5f4e 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,7 @@ PINECONE_API_KEY=your-pinecone-api-key-here # Required: Dense (hybrid) Pinecone index name (no default) PINECONE_INDEX_NAME= -# Optional: Reranking model. Unset is OK for CLI / alliance — bge-reranker-v2-m3 is applied automatically. -# Core-only (setupCoreServer) with no env var: reranking disabled. +# Optional: Reranking model (default: bge-reranker-v2-m3 when unset) PINECONE_RERANK_MODEL=bge-reranker-v2-m3 # Optional: Default number of results to return (default: 10) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cadb3..15f7ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### Changed -- **Breaking:** `PINECONE_INDEX_NAME` (or `--index-name`) is required; hardcoded Alliance **index** default removed. Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). Core has **no** rerank default; CLI and `resolveAllianceConfig` / `setupAllianceServer` still apply `bge-reranker-v2-m3` when `PINECONE_RERANK_MODEL` is unset so key+index-only MCP configs keep reranking. -- Hybrid `query` / `guided_query` set `rerank_skipped_reason: no_model` and `decision_trace.rerank_status: skipped_no_model` when reranking was requested but no rerank model is configured (core-only embedders). `setupCoreServer` logs a WARN when rerank is disabled. +- **Breaking:** `PINECONE_INDEX_NAME` (or `--index-name`) is required; hardcoded Alliance **index** default removed. Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). `resolveConfig` uses `PINECONE_RERANK_MODEL` when set, else defaults to `bge-reranker-v2-m3`. +- Hybrid `query` / `guided_query` set `rerank_skipped_reason: no_model` and `decision_trace.rerank_status: skipped_no_model` when reranking was requested but `PineconeClient` was constructed without a rerank model (manual library use). ### Added diff --git a/README.md b/README.md index 26e9e5f..fb367ca 100644 --- a/README.md +++ b/README.md @@ -93,18 +93,18 @@ The codebase is split into two layers: ## Configuration -You need a **Pinecone API key** and a **dense index name** (`PINECONE_INDEX_NAME`); sparse index defaults to `{index}-sparse`. **CLI and `setupAllianceServer`** apply the Alliance rerank default (`bge-reranker-v2-m3`) when `PINECONE_RERANK_MODEL` is unset so existing key+index-only MCP configs keep reranking. **Generic core** (`setupCoreServer` / package root) has no rerank default — set `PINECONE_RERANK_MODEL` or pass `rerankModel` explicitly. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every variable and CLI flag. +You need a **Pinecone API key** and a **dense index name** (`PINECONE_INDEX_NAME`); sparse index defaults to `{index}-sparse`. **Reranking** uses `PINECONE_RERANK_MODEL` when set; otherwise defaults to `bge-reranker-v2-m3` (same as typical MCP configs with only key + index). See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every variable and CLI flag. Quick reference: -| Variable | Required | Default | -| ----------------------------------- | ----------------------- | --------------------------------- | -| `PINECONE_API_KEY` | Yes (for live Pinecone) | — | -| `PINECONE_INDEX_NAME` | Yes | — (no default) | -| `PINECONE_RERANK_MODEL` | No (Alliance CLI default if unset) | Alliance: `bge-reranker-v2-m3`; core-only: disabled | -| `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | -| `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | -| `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | No | `text` (`json` for log pipelines) | +| Variable | Required | Default | +| ----------------------------------- | ---------------------------------- | --------------------------------------------------- | +| `PINECONE_API_KEY` | Yes (for live Pinecone) | — | +| `PINECONE_INDEX_NAME` | Yes | — (no default) | +| `PINECONE_RERANK_MODEL` | No | `bge-reranker-v2-m3` when unset | +| `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | +| `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | +| `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | No | `text` (`json` for log pipelines) | Run `pinecone-read-only-mcp --help` for CLI equivalents (`--cache-ttl-seconds`, `--request-timeout-ms`, `--disable-suggest-flow`, etc.). @@ -119,7 +119,7 @@ Treat **`setupCoreServer()` / `setupAllianceServer()` as one logical server per - **Generic bridge only:** `import { setupCoreServer, teardownServer, ... } from '@will-cppa/pinecone-read-only-mcp'` - **Full Alliance surface (CLI parity):** `import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'` -Recommended pattern for the full tool surface: `resolveAllianceConfig({ apiKey, indexName, ... })` → `setPineconeClient(new PineconeClient(...))` → `await setupAllianceServer(config)` → connect one MCP transport. Use `resolveConfig` only for **core-only** embedding (no implicit rerank default). See [examples/library-embedding-demo.ts](examples/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate). +Recommended pattern: `resolveConfig({ apiKey, indexName, ... })` → `setPineconeClient(new PineconeClient(...))` → `await setupAllianceServer(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). ### Custom URL generators @@ -130,15 +130,17 @@ Import `registerUrlGenerator` and types `UrlGeneratorFn` / `UrlGenerationResult` ```ts import { registerUrlGenerator, - resolveConfig, setPineconeClient, PineconeClient, type UrlGenerationResult, type UrlGeneratorFn, -} from '@will-cppa/pinecone-read-only-mcp'; -import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; +} from '`@will-cppa/pinecone-read-only-mcp`'; +import { + setupAllianceServer, + resolveAllianceConfig, +} from '`@will-cppa/pinecone-read-only-mcp/alliance`'; -const config = resolveConfig({ apiKey: '...', indexName: 'your-index' }); +const config = resolveAllianceConfig({ apiKey: '...', indexName: 'your-index' }); setPineconeClient(new PineconeClient({ apiKey: config.apiKey, indexName: config.indexName })); const server = await setupAllianceServer(config); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 76f363a..1c8c113 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -17,7 +17,7 @@ Configuration is built from **CLI flags** (when using the binary), **environment | `apiKey` | `apiKey` / `PINECONE_API_KEY` | **Required** (non-empty after trim) | | `indexName` | `indexName` / `PINECONE_INDEX_NAME` | **Required** (no default) | | `sparseIndexName` | `sparseIndexName` / `PINECONE_SPARSE_INDEX_NAME` | `{indexName}-sparse` | -| `rerankModel` | `rerankModel` / `PINECONE_RERANK_MODEL` | Core: unset disables reranking. Alliance CLI / `resolveAllianceConfig`: defaults to `bge-reranker-v2-m3` when unset | +| `rerankModel` | `rerankModel` / `PINECONE_RERANK_MODEL` | `bge-reranker-v2-m3` when env and overrides omit it | | `defaultTopK` | `defaultTopK` / `PINECONE_TOP_K` | `10` (positive int) | | `logLevel` | `logLevel` / `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | `INFO` (`DEBUG`–`ERROR`) | | `logFormat` | `logFormat` / `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | `text` or `json` | @@ -30,11 +30,9 @@ Configuration is built from **CLI flags** (when using the binary), **environment 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 call `setupAllianceServer(config)`. -### Alliance rerank default +### Rerank model -`resolveConfig` (core) never invents a rerank model. **`resolveAllianceConfig`** and **`setupAllianceServer`** call `applyAllianceRerankDefault()` so deployments that only set `PINECONE_API_KEY` and `PINECONE_INDEX_NAME` (typical MCP configs) still use `bge-reranker-v2-m3`. Override with `PINECONE_RERANK_MODEL` or `--rerank-model` anytime. - -Generic embedders using only `setupCoreServer` should set `PINECONE_RERANK_MODEL` explicitly or accept disabled reranking (startup WARN). +`resolveConfig` uses `PINECONE_RERANK_MODEL` (or `--rerank-model`) when set; otherwise **`bge-reranker-v2-m3`**. MCP configs that only set API key and index name keep reranking without extra env vars. --- @@ -60,7 +58,7 @@ Generic embedders using only `setupCoreServer` should set `PINECONE_RERANK_MODEL ## Library embedding -1. Build `ServerConfig` with `resolveAllianceConfig({ apiKey: '...', indexName: '...', ... })` for the full surface, or `resolveConfig` for core-only (no rerank default). +1. Build `ServerConfig` with `resolveConfig({ apiKey: '...', indexName: '...', ... })` or pass explicit overrides. 2. Construct `PineconeClient` and `setPineconeClient(client)` before `setupAllianceServer(config)` (mirrors `src/index.ts`). 3. `await setupAllianceServer(config)` (or `setupCoreServer` for generic tools only) then connect an MCP transport. diff --git a/examples/README.md b/examples/README.md index a662de2..b318d28 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,11 +1,11 @@ # Examples -| File | Description | -|------|-------------| -| [custom-url-generator.ts](./custom-url-generator.ts) | Embed the MCP server, register a **custom URL generator**, and call `setupAllianceServer()` (full tool surface). | -| [suggest-flow-demo.ts](./suggest-flow-demo.ts) | Document the **suggest_query_params → query** gate sequence and trimmed namespace usage. | -| [guided-query-demo.ts](./guided-query-demo.ts) | Document **guided_query** and the **`decision_trace`** payload. | -| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveConfig`, `setPineconeClient`, `setupAllianceServer`). | +| File | Description | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| [custom-url-generator.ts](./custom-url-generator.ts) | Embed the MCP server, register a **custom URL generator**, and call `setupAllianceServer()` (full tool surface). | +| [suggest-flow-demo.ts](./suggest-flow-demo.ts) | Document the **suggest_query_params → query** gate sequence and trimmed namespace usage. | +| [guided-query-demo.ts](./guided-query-demo.ts) | Document **guided_query** and the **`decision_trace`** payload. | +| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveAllianceConfig`, `setPineconeClient`, `setupAllianceServer`). | **Required env for live runs:** `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`. Optional: `PINECONE_RERANK_MODEL` (omit to disable reranking). diff --git a/examples/custom-url-generator.ts b/examples/custom-url-generator.ts index 393a862..859f450 100644 --- a/examples/custom-url-generator.ts +++ b/examples/custom-url-generator.ts @@ -18,13 +18,11 @@ import { PineconeClient, registerUrlGenerator, + resolveConfig, setPineconeClient, type UrlGenerationResult, } from '@will-cppa/pinecone-read-only-mcp'; -import { - resolveAllianceConfig, - setupAllianceServer, -} from '@will-cppa/pinecone-read-only-mcp/alliance'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; registerUrlGenerator('product-docs', (metadata): UrlGenerationResult => { const product = typeof metadata['product'] === 'string' ? metadata['product'] : null; @@ -44,8 +42,14 @@ registerUrlGenerator('product-docs', (metadata): UrlGenerationResult => { async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); - const indexName = process.env['PINECONE_INDEX_NAME']?.trim() ?? 'demo-index'; - const config = resolveAllianceConfig({ apiKey: apiKey ?? 'demo-key', indexName }); + const indexName = process.env['PINECONE_INDEX_NAME']?.trim(); + if (!apiKey || !indexName) { + console.log( + '[custom-url-generator] Set PINECONE_API_KEY and PINECONE_INDEX_NAME to run live.' + ); + return; + } + const config = resolveConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ diff --git a/examples/guided-query-demo.ts b/examples/guided-query-demo.ts index 6dbeb96..d0e96dd 100644 --- a/examples/guided-query-demo.ts +++ b/examples/guided-query-demo.ts @@ -15,11 +15,12 @@ * still returning HTTP/MCP success (see docs/TOOLS.md). */ -import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - resolveAllianceConfig, - setupAllianceServer, -} from '@will-cppa/pinecone-read-only-mcp/alliance'; + PineconeClient, + resolveConfig, + setPineconeClient, +} from '@will-cppa/pinecone-read-only-mcp'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); @@ -32,7 +33,7 @@ async function main(): Promise { return; } - const config = resolveAllianceConfig({ apiKey, indexName }); + const config = resolveConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ apiKey: config.apiKey, diff --git a/examples/library-embedding-demo.ts b/examples/library-embedding-demo.ts index e73d7c5..e0c2146 100644 --- a/examples/library-embedding-demo.ts +++ b/examples/library-embedding-demo.ts @@ -2,7 +2,7 @@ * Library embedding: build the MCP server from a Node script (not the CLI). * * Pattern (mirrors `src/index.ts`): - * 1. `resolveAllianceConfig({ apiKey, indexName, ... })` for full surface (includes rerank default). + * 1. `resolveConfig({ apiKey, indexName, ... })` — env rerank model or default bge-reranker-v2-m3. * 2. `new PineconeClient({ ... })` + `setPineconeClient(client)`. * 3. `await setupAllianceServer(config)` then `server.connect(transport)`. * @@ -12,11 +12,12 @@ * (tests). For isolated tenants in production, prefer one server per Node process. */ -import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - resolveAllianceConfig, - setupAllianceServer, -} from '@will-cppa/pinecone-read-only-mcp/alliance'; + PineconeClient, + resolveConfig, + setPineconeClient, +} from '@will-cppa/pinecone-read-only-mcp'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); @@ -31,7 +32,7 @@ async function main(): Promise { console.log('Set PINECONE_INDEX_NAME to run this example. Skipping live setup in doc-only mode.'); return; } - const config = resolveAllianceConfig({ apiKey, indexName }); + const config = resolveConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ diff --git a/examples/suggest-flow-demo.ts b/examples/suggest-flow-demo.ts index 84f30a2..e7f76e4 100644 --- a/examples/suggest-flow-demo.ts +++ b/examples/suggest-flow-demo.ts @@ -16,11 +16,12 @@ * `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`, then wire an MCP transport. */ -import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp'; import { - resolveAllianceConfig, - setupAllianceServer, -} from '@will-cppa/pinecone-read-only-mcp/alliance'; + PineconeClient, + resolveConfig, + setPineconeClient, +} from '@will-cppa/pinecone-read-only-mcp'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; async function main(): Promise { const apiKey = process.env['PINECONE_API_KEY']?.trim(); @@ -33,7 +34,7 @@ async function main(): Promise { return; } - const config = resolveAllianceConfig({ apiKey, indexName }); + const config = resolveConfig({ apiKey, indexName }); setPineconeClient( new PineconeClient({ apiKey: config.apiKey, diff --git a/src/alliance/config.test.ts b/src/alliance/config.test.ts index 11d3c2c..098184f 100644 --- a/src/alliance/config.test.ts +++ b/src/alliance/config.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - applyAllianceRerankDefault, - DEFAULT_ALLIANCE_RERANK_MODEL, - resolveAllianceConfig, -} from './config.js'; +import { DEFAULT_RERANK_MODEL } from '../core/config.js'; +import { resolveAllianceConfig } from './config.js'; describe('resolveAllianceConfig', () => { it('applies Alliance rerank default when env and overrides omit rerankModel', () => { @@ -11,7 +8,7 @@ describe('resolveAllianceConfig', () => { { apiKey: 'sk-test', indexName: 'my-index' }, { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } ); - expect(cfg.rerankModel).toBe(DEFAULT_ALLIANCE_RERANK_MODEL); + expect(cfg.rerankModel).toBe(DEFAULT_RERANK_MODEL); }); it('preserves explicit rerankModel from overrides', () => { @@ -35,22 +32,3 @@ describe('resolveAllianceConfig', () => { expect(cfg.rerankModel).toBe('env-reranker'); }); }); - -describe('applyAllianceRerankDefault', () => { - it('does not replace an existing rerankModel', () => { - const cfg = applyAllianceRerankDefault({ - apiKey: 'k', - indexName: 'i', - sparseIndexName: 'i-sparse', - rerankModel: 'already-set', - defaultTopK: 10, - logLevel: 'INFO', - logFormat: 'text', - cacheTtlMs: 1800_000, - requestTimeoutMs: 15_000, - disableSuggestFlow: false, - checkIndexes: false, - }); - expect(cfg.rerankModel).toBe('already-set'); - }); -}); diff --git a/src/alliance/config.ts b/src/alliance/config.ts index f891afa..d659bda 100644 --- a/src/alliance/config.ts +++ b/src/alliance/config.ts @@ -1,33 +1,27 @@ /** - * Alliance-layer configuration: wraps {@link resolveConfig} with deployment defaults - * that must not live in the generic core package. + * Alliance entry re-exports core config. Rerank default lives in {@link resolveConfig} + * (`PINECONE_RERANK_MODEL` when set, else `bge-reranker-v2-m3`). */ -import { resolveConfig, type ConfigOverrides, type ServerConfig } from '../core/config.js'; +import { + DEFAULT_RERANK_MODEL, + resolveConfig, + type ConfigOverrides, + type ServerConfig, +} from '../core/config.js'; -/** Default Pinecone inference rerank model for C++ Alliance deployments (CLI / alliance entry). */ -export const DEFAULT_ALLIANCE_RERANK_MODEL = 'bge-reranker-v2-m3'; +/** @deprecated Use {@link DEFAULT_RERANK_MODEL} from core config. */ +export const DEFAULT_ALLIANCE_RERANK_MODEL = DEFAULT_RERANK_MODEL; -/** - * Apply the Alliance rerank default when no model was provided via env or overrides. - * Core {@link resolveConfig} intentionally leaves `rerankModel` unset so generic adopters - * do not silently use an Alliance-specific model name. - */ +/** @deprecated No-op; {@link resolveConfig} already applies the rerank default. */ export function applyAllianceRerankDefault(config: ServerConfig): ServerConfig { - if (config.rerankModel !== undefined) { - return config; - } - return { ...config, rerankModel: DEFAULT_ALLIANCE_RERANK_MODEL }; + return config; } -/** - * Build {@link ServerConfig} for the full Alliance MCP surface (CLI and `setupAllianceServer`). - * Same requirements as core (`apiKey`, `indexName`); supplies {@link DEFAULT_ALLIANCE_RERANK_MODEL} - * when `PINECONE_RERANK_MODEL` and `rerankModel` overrides are absent. - */ +/** Alias for {@link resolveConfig} (Alliance CLI and `setupAllianceServer`). */ export function resolveAllianceConfig( overrides: ConfigOverrides = {}, env: NodeJS.ProcessEnv = process.env ): ServerConfig { - return applyAllianceRerankDefault(resolveConfig(overrides, env)); + return resolveConfig(overrides, env); } diff --git a/src/alliance/setup.ts b/src/alliance/setup.ts index d52c50e..e470ba1 100644 --- a/src/alliance/setup.ts +++ b/src/alliance/setup.ts @@ -2,7 +2,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { ServerConfig } from '../core/config.js'; import { resolveConfig } from '../core/config.js'; import { setupCoreServer } from '../core/setup.js'; -import { applyAllianceRerankDefault } from './config.js'; import { registerBuiltinUrlGenerators } from './url-builtins.js'; import { registerGuidedQueryTool } from './tools/guided-query-tool.js'; import { registerSuggestQueryParamsTool } from './tools/suggest-query-params-tool.js'; @@ -11,12 +10,10 @@ import { registerSuggestQueryParamsTool } from './tools/suggest-query-params-too * Create and configure the MCP server with the full Alliance tool surface: * all core tools plus `suggest_query_params`, `guided_query`, and built-in URL generators. * - * When `config` is omitted, resolves env via {@link resolveConfig} then applies the - * Alliance rerank default. When `config` is passed, applies the default if `rerankModel` is unset. + * When `config` is omitted, resolves env via {@link resolveConfig} (rerank: env or default). */ export async function setupAllianceServer(config?: ServerConfig): Promise { - const allianceConfig = applyAllianceRerankDefault(config ?? resolveConfig({})); - const server = await setupCoreServer(allianceConfig); + const server = await setupCoreServer(config ?? resolveConfig({})); registerBuiltinUrlGenerators(); registerSuggestQueryParamsTool(server); registerGuidedQueryTool(server); diff --git a/src/cli.ts b/src/cli.ts index 6dd5a46..b1db0e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -138,7 +138,7 @@ Options: --api-key TEXT Pinecone API key (or PINECONE_API_KEY) --index-name TEXT Dense index (required, or PINECONE_INDEX_NAME) --sparse-index-name TEXT Sparse index [default: {index-name}-sparse] - --rerank-model TEXT Reranker model (optional; omit to disable reranking) + --rerank-model TEXT Reranker model (default: bge-reranker-v2-m3, or PINECONE_RERANK_MODEL) --top-k N Default top-k for queries [env: PINECONE_TOP_K] --log-level LEVEL DEBUG | INFO | WARN | ERROR [default: INFO] --log-format FORMAT text | json [default: text] diff --git a/src/core/config.test.ts b/src/core/config.test.ts index 8e5a3a8..44e3c7b 100644 --- a/src/core/config.test.ts +++ b/src/core/config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { resolveConfig } from './config.js'; +import { DEFAULT_RERANK_MODEL, resolveConfig } from './config.js'; describe('resolveConfig', () => { it('throws when API key is missing', () => { @@ -16,14 +16,26 @@ describe('resolveConfig', () => { expect(() => resolveConfig({ apiKey: 'sk-test' }, {})).toThrow(/Missing Pinecone index name/); }); - it('leaves rerankModel undefined when env and overrides omit it', () => { + it('uses DEFAULT_RERANK_MODEL when env and overrides omit rerankModel', () => { const cfg = resolveConfig( { apiKey: 'sk-test', indexName: 'my-index' }, { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } ); expect(cfg.indexName).toBe('my-index'); expect(cfg.sparseIndexName).toBe('my-index-sparse'); - expect(cfg.rerankModel).toBeUndefined(); + expect(cfg.rerankModel).toBe(DEFAULT_RERANK_MODEL); + }); + + it('uses PINECONE_RERANK_MODEL from env when set', () => { + const cfg = resolveConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { + PINECONE_API_KEY: 'sk-test', + PINECONE_INDEX_NAME: 'my-index', + PINECONE_RERANK_MODEL: 'env-reranker', + } + ); + expect(cfg.rerankModel).toBe('env-reranker'); }); it('sets rerankModel when provided', () => { diff --git a/src/core/config.ts b/src/core/config.ts index 85571c3..edb8056 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -27,8 +27,8 @@ export interface ServerConfig { indexName: string; /** Sparse index name. Defaults to `${indexName}-sparse`. */ sparseIndexName: string; - /** Reranker model identifier. When unset, reranking is disabled. */ - rerankModel?: string; + /** Reranker model identifier (`PINECONE_RERANK_MODEL` or {@link DEFAULT_RERANK_MODEL}). */ + rerankModel: string; /** Default top-k when callers omit it on `query`. */ defaultTopK: number; /** Minimum log level emitted to stderr. */ @@ -48,6 +48,9 @@ export interface ServerConfig { /** Default per-call timeout for Pinecone requests, in milliseconds. */ export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000; +/** Default Pinecone inference rerank model when `PINECONE_RERANK_MODEL` is unset. */ +export const DEFAULT_RERANK_MODEL = 'bge-reranker-v2-m3'; + function asLogLevel(value: string | undefined, fallback: LogLevel): LogLevel { const allowed: LogLevel[] = ['DEBUG', 'INFO', 'WARN', 'ERROR']; return allowed.includes(value as LogLevel) ? (value as LogLevel) : fallback; @@ -118,7 +121,8 @@ export function resolveConfig( trimOptional(overrides.sparseIndexName ?? env['PINECONE_SPARSE_INDEX_NAME']) ?? `${indexName}-sparse`; - const rerankModel = trimOptional(overrides.rerankModel ?? env['PINECONE_RERANK_MODEL']); + const rerankModel = + trimOptional(overrides.rerankModel ?? env['PINECONE_RERANK_MODEL']) ?? DEFAULT_RERANK_MODEL; const defaultTopK = overrides.defaultTopK ?? asPositiveInt(env['PINECONE_TOP_K'], DEFAULT_TOP_K); const logLevel = asLogLevel( @@ -143,7 +147,7 @@ export function resolveConfig( apiKey, indexName, sparseIndexName, - ...(rerankModel !== undefined ? { rerankModel } : {}), + rerankModel, defaultTopK, logLevel, logFormat, diff --git a/src/core/index.ts b/src/core/index.ts index 3fdf7d5..5501de8 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -23,7 +23,7 @@ export { hasUrlGenerator, } from './server/url-registry.js'; export type { UrlGenerationResult, UrlGenerator, UrlGeneratorFn } from './server/url-registry.js'; -export { resolveConfig } from './config.js'; +export { resolveConfig, DEFAULT_RERANK_MODEL } from './config.js'; export type { ServerConfig, LogLevel, LogFormat, ConfigOverrides } from './config.js'; export { PineconeClient } from './pinecone-client.js'; export type { diff --git a/src/core/pinecone-client.ts b/src/core/pinecone-client.ts index 7699226..2ce3744 100644 --- a/src/core/pinecone-client.ts +++ b/src/core/pinecone-client.ts @@ -37,7 +37,8 @@ export class PineconeClient { */ constructor(config: PineconeClientConfig) { this.indexSession = new PineconeIndexSession(config.apiKey, config.indexName); - this.rerankModel = config.rerankModel; + const normalizedRerankModel = config.rerankModel?.trim(); + this.rerankModel = normalizedRerankModel ? normalizedRerankModel : undefined; this.defaultTopK = config.defaultTopK ?? DEFAULT_TOP_K; } diff --git a/src/core/server/url-registry.ts b/src/core/server/url-registry.ts index bc5efe2..017e8bb 100644 --- a/src/core/server/url-registry.ts +++ b/src/core/server/url-registry.ts @@ -68,12 +68,12 @@ export function registerUrlGenerator(namespace: string, generator: UrlGeneratorF /** Remove a namespace's URL generator. Returns true if a generator was removed. */ export function unregisterUrlGenerator(namespace: string): boolean { - return urlGenerators.delete(namespace); + return urlGenerators.delete(namespace.trim()); } /** True when the namespace has a registered URL generator (does not consider `metadata.url`). */ export function hasUrlGenerator(namespace: string): boolean { - return urlGenerators.has(namespace); + return urlGenerators.has(namespace.trim()); } /** @@ -89,7 +89,7 @@ export function generateUrlForNamespace( return { url: existingUrl, method: 'metadata.url' }; } - const generator = urlGenerators.get(namespace); + const generator = urlGenerators.get(namespace.trim()); if (generator) { return generator(metadata); } diff --git a/src/core/setup.ts b/src/core/setup.ts index ecfc337..b1776ae 100644 --- a/src/core/setup.ts +++ b/src/core/setup.ts @@ -1,6 +1,5 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION } from '../constants.js'; -import { warn as logWarn } from '../logger.js'; import type { ServerConfig } from './config.js'; import { clearPineconeClient } from './server/client-context.js'; import { setServerConfig, resetServerConfig } from './server/config-context.js'; @@ -46,12 +45,6 @@ export async function setupCoreServer(config?: ServerConfig): Promise if (config) { setServerConfig(config); - if (!config.rerankModel) { - logWarn( - 'Reranking disabled: set PINECONE_RERANK_MODEL or pass rerankModel. ' + - 'Alliance deployments: use resolveAllianceConfig / setupAllianceServer for the default rerank model.' - ); - } } const server = new McpServer( diff --git a/src/index.ts b/src/index.ts index 67610d4..faa05b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,14 @@ /** * Pinecone Read-Only MCP CLI entry point. * - * Thin composition root: parseCli() -> resolveAllianceConfig() -> setupAllianceServer(config) + * Thin composition root: parseCli() -> resolveConfig() -> setupAllianceServer(config) * -> connect to stdio transport. */ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import * as dotenv from 'dotenv'; import { parseCli, printHelp, printVersion } from './cli.js'; -import { resolveAllianceConfig } from './alliance/config.js'; -import type { ServerConfig } from './core/config.js'; +import { resolveConfig, type ServerConfig } from './core/config.js'; import { PineconeClient } from './core/pinecone-client.js'; import { setPineconeClient } from './core/server/client-context.js'; import { setupAllianceServer } from './alliance/setup.js'; @@ -35,7 +34,7 @@ function buildConfigOrExit(): ServerConfig { } try { - return resolveAllianceConfig(parsed.overrides); + return resolveConfig(parsed.overrides); } catch (err) { const message = err instanceof Error ? err.message : String(err); process.stderr.write(`Error: ${message}\n`); From 665a6b46dc9dcddc79149d51fba71b8b398da63c Mon Sep 17 00:00:00 2001 From: zho Date: Wed, 27 May 2026 22:00:31 +0800 Subject: [PATCH 4/6] addressed first reviewer's suggestions --- .env.example | 2 +- CHANGELOG.md | 2 +- README.md | 12 ++++---- examples/README.md | 2 +- src/core/pinecone-client.ts | 2 +- src/core/server/tools/query-documents-tool.ts | 3 ++ src/core/server/tools/query-tool.test.ts | 28 +++++++++++++++++++ src/core/server/tools/query-tool.ts | 3 ++ src/types.ts | 4 ++- 9 files changed, 46 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 4ac5f4e..226b06e 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ PINECONE_API_KEY=your-pinecone-api-key-here # Required: Dense (hybrid) Pinecone index name (no default) -PINECONE_INDEX_NAME= +PINECONE_INDEX_NAME=my-index-name # Optional: Reranking model (default: bge-reranker-v2-m3 when unset) PINECONE_RERANK_MODEL=bge-reranker-v2-m3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f7ec7..083eaef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### Changed - **Breaking:** `PINECONE_INDEX_NAME` (or `--index-name`) is required; hardcoded Alliance **index** default removed. Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). `resolveConfig` uses `PINECONE_RERANK_MODEL` when set, else defaults to `bge-reranker-v2-m3`. -- Hybrid `query` / `guided_query` set `rerank_skipped_reason: no_model` and `decision_trace.rerank_status: skipped_no_model` when reranking was requested but `PineconeClient` was constructed without a rerank model (manual library use). +- When reranking was requested but `PineconeClient` has no rerank model (manual library use): `query` / `query_documents` include `rerank_skipped_reason: no_model`; `guided_query` sets `decision_trace.rerank_status: skipped_no_model`. ### Added diff --git a/README.md b/README.md index fb367ca..b1e159b 100644 --- a/README.md +++ b/README.md @@ -129,18 +129,16 @@ Import `registerUrlGenerator` and types `UrlGeneratorFn` / `UrlGenerationResult` ```ts import { + PineconeClient, registerUrlGenerator, + resolveConfig, setPineconeClient, - PineconeClient, type UrlGenerationResult, type UrlGeneratorFn, -} from '`@will-cppa/pinecone-read-only-mcp`'; -import { - setupAllianceServer, - resolveAllianceConfig, -} from '`@will-cppa/pinecone-read-only-mcp/alliance`'; +} from '@will-cppa/pinecone-read-only-mcp'; +import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance'; -const config = resolveAllianceConfig({ apiKey: '...', indexName: 'your-index' }); +const config = resolveConfig({ apiKey: '...', indexName: 'your-index' }); setPineconeClient(new PineconeClient({ apiKey: config.apiKey, indexName: config.indexName })); const server = await setupAllianceServer(config); diff --git a/examples/README.md b/examples/README.md index b318d28..cca4b85 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ | [custom-url-generator.ts](./custom-url-generator.ts) | Embed the MCP server, register a **custom URL generator**, and call `setupAllianceServer()` (full tool surface). | | [suggest-flow-demo.ts](./suggest-flow-demo.ts) | Document the **suggest_query_params → query** gate sequence and trimmed namespace usage. | | [guided-query-demo.ts](./guided-query-demo.ts) | Document **guided_query** and the **`decision_trace`** payload. | -| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveAllianceConfig`, `setPineconeClient`, `setupAllianceServer`). | +| [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveConfig`, `setPineconeClient`, `setupAllianceServer`). | **Required env for live runs:** `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`. Optional: `PINECONE_RERANK_MODEL` (omit to disable reranking). diff --git a/src/core/pinecone-client.ts b/src/core/pinecone-client.ts index 2ce3744..85ce4b7 100644 --- a/src/core/pinecone-client.ts +++ b/src/core/pinecone-client.ts @@ -181,7 +181,7 @@ export class PineconeClient { if (useReranking && this.rerankModel === undefined) { rerank_skipped_reason = 'no_model'; degradation_reason = - 'rerank_skipped_no_model: set PINECONE_RERANK_MODEL, pass rerankModel in config, or use resolveAllianceConfig / setupAllianceServer for the Alliance default rerank model.'; + 'rerank_skipped_no_model: set PINECONE_RERANK_MODEL, pass rerankModel in config, or construct PineconeClient from resolveConfig().'; } } diff --git a/src/core/server/tools/query-documents-tool.ts b/src/core/server/tools/query-documents-tool.ts index b115ecc..9e1c041 100644 --- a/src/core/server/tools/query-documents-tool.ts +++ b/src/core/server/tools/query-documents-tool.ts @@ -136,6 +136,9 @@ export function registerQueryDocumentsTool(server: McpServer): void { ? { degradation_reason: queryOutcome.degradation_reason } : {}), hybrid_leg_failed: queryOutcome.hybrid_leg_failed, + ...(queryOutcome.rerank_skipped_reason !== undefined + ? { rerank_skipped_reason: queryOutcome.rerank_skipped_reason } + : {}), documents: topDocuments.map((doc) => ({ document_id: doc.document_id, merged_content: doc.merged_content, diff --git a/src/core/server/tools/query-tool.test.ts b/src/core/server/tools/query-tool.test.ts index ae8ca21..ec18544 100644 --- a/src/core/server/tools/query-tool.test.ts +++ b/src/core/server/tools/query-tool.test.ts @@ -217,6 +217,34 @@ describe('query tool handler (preset-driven)', () => { expect(query).not.toHaveBeenCalled(); }); + it('query: forwards rerank_skipped_reason when rerank was skipped (no model)', async () => { + mockedGetClient.mockReturnValue({ + query: vi.fn().mockResolvedValue( + makeHybridQueryResult({ + rerank_skipped_reason: 'no_model', + degradation_reason: 'rerank_skipped_no_model: set PINECONE_RERANK_MODEL', + }) + ), + count: vi.fn(), + } as never); + + const server = createMockServer(); + registerQueryTool(server as never); + + const body = parseToolJson( + await server.getHandler('query')!({ + query_text: 'hello', + namespace: 'wg21', + top_k: 3, + preset: 'detailed', + }) + ); + + expect(body.status).toBe('success'); + expect(body.rerank_skipped_reason).toBe('no_model'); + expect(body.degradation_reason).toMatch(/rerank_skipped_no_model/); + }); + it('query: surfaces unreranked hits when client returns reranked:false (rerank fallback shape)', async () => { mockedGetClient.mockReturnValue({ query: vi.fn().mockResolvedValue( diff --git a/src/core/server/tools/query-tool.ts b/src/core/server/tools/query-tool.ts index e19ba87..7412532 100644 --- a/src/core/server/tools/query-tool.ts +++ b/src/core/server/tools/query-tool.ts @@ -84,6 +84,9 @@ async function executeQuery(params: QueryExecParams) { ? { degradation_reason: queryOutcome.degradation_reason } : {}), hybrid_leg_failed: queryOutcome.hybrid_leg_failed, + ...(queryOutcome.rerank_skipped_reason !== undefined + ? { rerank_skipped_reason: queryOutcome.rerank_skipped_reason } + : {}), }; return jsonResponse(response); } catch (error) { diff --git a/src/types.ts b/src/types.ts index 74066f4..28fd8b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,7 +53,7 @@ export interface HybridQueryResult { hybrid_leg_failed: HybridLegFailed; /** * Set when `useReranking` was true but no rerank model is configured on the client - * (generic core embedders). Alliance CLI uses {@link resolveAllianceConfig} to avoid this. + * (manual `PineconeClient` without `rerankModel`). Normal MCP/CLI use sets a model via {@link resolveConfig}. */ rerank_skipped_reason?: RerankSkippedReason; } @@ -162,6 +162,8 @@ export interface QueryResponse { degradation_reason?: string; /** Partial hybrid failure: one leg failed while the other returned hits. */ hybrid_leg_failed?: HybridLegFailed; + /** Present when reranking was requested but no rerank model is on the client. */ + rerank_skipped_reason?: RerankSkippedReason; } /** Internal merged hit shape before rerank (dense + sparse deduped). */ From c893fc140a16aa6431557011a8405f0455cc4b3c Mon Sep 17 00:00:00 2001 From: zho Date: Wed, 27 May 2026 23:07:35 +0800 Subject: [PATCH 5/6] update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b1e159b..104b9d6 100644 --- a/README.md +++ b/README.md @@ -97,14 +97,14 @@ You need a **Pinecone API key** and a **dense index name** (`PINECONE_INDEX_NAME Quick reference: -| Variable | Required | Default | -| ----------------------------------- | ---------------------------------- | --------------------------------------------------- | -| `PINECONE_API_KEY` | Yes (for live Pinecone) | — | -| `PINECONE_INDEX_NAME` | Yes | — (no default) | +| Variable | Required | Default | +| ----------------------------------- | ----------------------- | --------------------------------- | +| `PINECONE_API_KEY` | Yes (for live Pinecone) | — | +| `PINECONE_INDEX_NAME` | Yes | — (no default) | | `PINECONE_RERANK_MODEL` | No | `bge-reranker-v2-m3` when unset | -| `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | -| `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | -| `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | No | `text` (`json` for log pipelines) | +| `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | +| `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | +| `PINECONE_READ_ONLY_MCP_LOG_FORMAT` | No | `text` (`json` for log pipelines) | Run `pinecone-read-only-mcp --help` for CLI equivalents (`--cache-ttl-seconds`, `--request-timeout-ms`, `--disable-suggest-flow`, etc.). @@ -247,7 +247,7 @@ node node_modules/@will-cppa/pinecone-read-only-mcp/dist/index.js --api-key YOUR ``` --api-key TEXT Pinecone API key (or set PINECONE_API_KEY env var) --index-name TEXT Dense index (required, or PINECONE_INDEX_NAME) ---rerank-model TEXT Reranker model (optional; omit to disable reranking) +--rerank-model TEXT Reranker model (defalut: bge-reranker-v2-m3) --log-level TEXT Logging level [default: INFO] --help, -h Show help message ``` From 7757de23b59b64b31cbe30c7b88fbf7594eafdad Mon Sep 17 00:00:00 2001 From: zho Date: Thu, 28 May 2026 01:12:02 +0800 Subject: [PATCH 6/6] updated to introduce default index name --- .env.example | 4 ++-- CHANGELOG.md | 2 +- README.md | 4 ++-- docs/CONFIGURATION.md | 6 +++--- examples/README.md | 2 +- src/cli.ts | 2 +- src/core/config.test.ts | 31 +++++++++++++++++++++---------- src/core/config.ts | 15 ++++++++------- src/core/index.ts | 2 +- src/types.ts | 3 ++- 10 files changed, 42 insertions(+), 29 deletions(-) diff --git a/.env.example b/.env.example index 226b06e..6a70808 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,8 @@ # Required: Your Pinecone API key PINECONE_API_KEY=your-pinecone-api-key-here -# Required: Dense (hybrid) Pinecone index name (no default) -PINECONE_INDEX_NAME=my-index-name +# Optional: Dense (hybrid) Pinecone index name (default: rag-hybrid when unset) +# PINECONE_INDEX_NAME=rag-hybrid # Optional: Reranking model (default: bge-reranker-v2-m3 when unset) PINECONE_RERANK_MODEL=bge-reranker-v2-m3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 083eaef..23d64db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### Changed -- **Breaking:** `PINECONE_INDEX_NAME` (or `--index-name`) is required; hardcoded Alliance **index** default removed. Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). `resolveConfig` uses `PINECONE_RERANK_MODEL` when set, else defaults to `bge-reranker-v2-m3`. +- Package root export is the generic **core** layer (`setupCoreServer`); full CLI parity uses `@will-cppa/pinecone-read-only-mcp/alliance` (`setupAllianceServer`, built-in URL generators). `resolveConfig` uses env when set, else defaults: index **`rag-hybrid`**, rerank **`bge-reranker-v2-m3`** (constants `DEFAULT_INDEX_NAME` / `DEFAULT_RERANK_MODEL` in `src/core/config.ts`). - When reranking was requested but `PineconeClient` has no rerank model (manual library use): `query` / `query_documents` include `rerank_skipped_reason: no_model`; `guided_query` sets `decision_trace.rerank_status: skipped_no_model`. ### Added diff --git a/README.md b/README.md index 104b9d6..ff770da 100644 --- a/README.md +++ b/README.md @@ -93,14 +93,14 @@ The codebase is split into two layers: ## Configuration -You need a **Pinecone API key** and a **dense index name** (`PINECONE_INDEX_NAME`); sparse index defaults to `{index}-sparse`. **Reranking** uses `PINECONE_RERANK_MODEL` when set; otherwise defaults to `bge-reranker-v2-m3` (same as typical MCP configs with only key + index). See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every variable and CLI flag. +You need a **Pinecone API key**. **Index** uses `PINECONE_INDEX_NAME` when set, else defaults to `rag-hybrid`; sparse index defaults to `{index}-sparse`. **Reranking** uses `PINECONE_RERANK_MODEL` when set, else `bge-reranker-v2-m3`. MCP configs with only `PINECONE_API_KEY` keep prior Alliance defaults. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for every variable and CLI flag. Quick reference: | Variable | Required | Default | | ----------------------------------- | ----------------------- | --------------------------------- | | `PINECONE_API_KEY` | Yes (for live Pinecone) | — | -| `PINECONE_INDEX_NAME` | Yes | — (no default) | +| `PINECONE_INDEX_NAME` | No | `rag-hybrid` when unset | | `PINECONE_RERANK_MODEL` | No | `bge-reranker-v2-m3` when unset | | `PINECONE_SPARSE_INDEX_NAME` | No | `{index}-sparse` | | `PINECONE_READ_ONLY_MCP_LOG_LEVEL` | No | `INFO` (`DEBUG`–`ERROR`) | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1c8c113..ae4ff80 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -15,7 +15,7 @@ Configuration is built from **CLI flags** (when using the binary), **environment | Field | Source | Default / notes | | ----- | ------ | --------------- | | `apiKey` | `apiKey` / `PINECONE_API_KEY` | **Required** (non-empty after trim) | -| `indexName` | `indexName` / `PINECONE_INDEX_NAME` | **Required** (no default) | +| `indexName` | `indexName` / `PINECONE_INDEX_NAME` | `rag-hybrid` when env and overrides omit it | | `sparseIndexName` | `sparseIndexName` / `PINECONE_SPARSE_INDEX_NAME` | `{indexName}-sparse` | | `rerankModel` | `rerankModel` / `PINECONE_RERANK_MODEL` | `bge-reranker-v2-m3` when env and overrides omit it | | `defaultTopK` | `defaultTopK` / `PINECONE_TOP_K` | `10` (positive int) | @@ -26,13 +26,13 @@ Configuration is built from **CLI flags** (when using the binary), **environment | `disableSuggestFlow` | `disableSuggestFlow` / `PINECONE_DISABLE_SUGGEST_FLOW` | `false` (bool parsing: true/1/yes/on) | | `checkIndexes` | `checkIndexes` / `PINECONE_CHECK_INDEXES` | `false` | -**Throws** if `apiKey` or `indexName` is missing after trim. +**Throws** if `apiKey` is missing after trim. 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 call `setupAllianceServer(config)`. ### Rerank model -`resolveConfig` uses `PINECONE_RERANK_MODEL` (or `--rerank-model`) when set; otherwise **`bge-reranker-v2-m3`**. MCP configs that only set API key and index name keep reranking without extra env vars. +`resolveConfig` uses `PINECONE_INDEX_NAME` / `PINECONE_RERANK_MODEL` when set; otherwise **`rag-hybrid`** and **`bge-reranker-v2-m3`**. MCP configs that only set `PINECONE_API_KEY` keep the same defaults as before. --- diff --git a/examples/README.md b/examples/README.md index cca4b85..8d81459 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,6 @@ | [guided-query-demo.ts](./guided-query-demo.ts) | Document **guided_query** and the **`decision_trace`** payload. | | [library-embedding-demo.ts](./library-embedding-demo.ts) | Minimal **library embedding** (`resolveConfig`, `setPineconeClient`, `setupAllianceServer`). | -**Required env for live runs:** `PINECONE_API_KEY` and `PINECONE_INDEX_NAME`. Optional: `PINECONE_RERANK_MODEL` (omit to disable reranking). +**Required env for live runs:** `PINECONE_API_KEY`. Optional: `PINECONE_INDEX_NAME` (default `rag-hybrid`), `PINECONE_RERANK_MODEL` (default `bge-reranker-v2-m3`). Run with `npm run build` then `npx tsx examples/.ts` from the repo root (examples resolve the package via `dist/core` and `dist/alliance`). diff --git a/src/cli.ts b/src/cli.ts index b1db0e5..e5eedb6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -136,7 +136,7 @@ Usage: pinecone-read-only-mcp [options] Options: --api-key TEXT Pinecone API key (or PINECONE_API_KEY) - --index-name TEXT Dense index (required, or PINECONE_INDEX_NAME) + --index-name TEXT Dense index (default: rag-hybrid, or PINECONE_INDEX_NAME) --sparse-index-name TEXT Sparse index [default: {index-name}-sparse] --rerank-model TEXT Reranker model (default: bge-reranker-v2-m3, or PINECONE_RERANK_MODEL) --top-k N Default top-k for queries [env: PINECONE_TOP_K] diff --git a/src/core/config.test.ts b/src/core/config.test.ts index 44e3c7b..d6da729 100644 --- a/src/core/config.test.ts +++ b/src/core/config.test.ts @@ -1,19 +1,32 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_RERANK_MODEL, resolveConfig } from './config.js'; +import { DEFAULT_INDEX_NAME, DEFAULT_RERANK_MODEL, resolveConfig } from './config.js'; describe('resolveConfig', () => { it('throws when API key is missing', () => { expect(() => resolveConfig({}, { PINECONE_API_KEY: '' })).toThrow(/Missing Pinecone API key/); }); - it('throws when index name is missing', () => { - expect(() => - resolveConfig({ apiKey: 'sk-test' }, { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: '' }) - ).toThrow(/Missing Pinecone index name/); + it('uses DEFAULT_INDEX_NAME when env and overrides omit indexName', () => { + const cfg = resolveConfig({ apiKey: 'sk-test' }, { PINECONE_API_KEY: 'sk-test' }); + expect(cfg.indexName).toBe(DEFAULT_INDEX_NAME); + expect(cfg.sparseIndexName).toBe(`${DEFAULT_INDEX_NAME}-sparse`); }); - it('requires index from overrides when env is unset', () => { - expect(() => resolveConfig({ apiKey: 'sk-test' }, {})).toThrow(/Missing Pinecone index name/); + it('uses PINECONE_INDEX_NAME from env when set', () => { + const cfg = resolveConfig( + { apiKey: 'sk-test' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + expect(cfg.indexName).toBe('my-index'); + expect(cfg.sparseIndexName).toBe('my-index-sparse'); + }); + + it('uses DEFAULT_INDEX_NAME when env index is empty after trim', () => { + const cfg = resolveConfig( + { apiKey: 'sk-test' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: ' ' } + ); + expect(cfg.indexName).toBe(DEFAULT_INDEX_NAME); }); it('uses DEFAULT_RERANK_MODEL when env and overrides omit rerankModel', () => { @@ -21,8 +34,6 @@ describe('resolveConfig', () => { { apiKey: 'sk-test', indexName: 'my-index' }, { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } ); - expect(cfg.indexName).toBe('my-index'); - expect(cfg.sparseIndexName).toBe('my-index-sparse'); expect(cfg.rerankModel).toBe(DEFAULT_RERANK_MODEL); }); @@ -38,7 +49,7 @@ describe('resolveConfig', () => { expect(cfg.rerankModel).toBe('env-reranker'); }); - it('sets rerankModel when provided', () => { + it('sets rerankModel when provided via overrides', () => { const cfg = resolveConfig({ apiKey: 'sk-test', indexName: 'my-index', diff --git a/src/core/config.ts b/src/core/config.ts index edb8056..0e2e08a 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -18,12 +18,12 @@ export type LogFormat = 'text' | 'json'; * Unified runtime configuration for the MCP server. * * Built once by `parseCli()` (or constructed directly by library consumers) - * and threaded through setup. `apiKey` and `indexName` are required. + * and threaded through setup. `apiKey` is required; `indexName` defaults via {@link DEFAULT_INDEX_NAME}. */ export interface ServerConfig { /** Pinecone API key. Required. */ apiKey: string; - /** Dense (hybrid) index name. Required. */ + /** Dense (hybrid) index name (`PINECONE_INDEX_NAME` or {@link DEFAULT_INDEX_NAME}). */ indexName: string; /** Sparse index name. Defaults to `${indexName}-sparse`. */ sparseIndexName: string; @@ -51,6 +51,9 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000; /** Default Pinecone inference rerank model when `PINECONE_RERANK_MODEL` is unset. */ export const DEFAULT_RERANK_MODEL = 'bge-reranker-v2-m3'; +/** Default dense index name when `PINECONE_INDEX_NAME` is unset. */ +export const DEFAULT_INDEX_NAME = 'rag-hybrid'; + function asLogLevel(value: string | undefined, fallback: LogLevel): LogLevel { const allowed: LogLevel[] = ['DEBUG', 'INFO', 'WARN', 'ERROR']; return allowed.includes(value as LogLevel) ? (value as LogLevel) : fallback; @@ -99,7 +102,7 @@ export interface ConfigOverrides { * Build a `ServerConfig` from CLI overrides, environment variables, and defaults. * CLI > env > default precedence is preserved. * - * @throws Error when no API key or index name is provided. + * @throws Error when no API key is provided. */ export function resolveConfig( overrides: ConfigOverrides, @@ -112,10 +115,8 @@ export function resolveConfig( ); } - const indexName = trimOptional(overrides.indexName ?? env['PINECONE_INDEX_NAME']); - if (!indexName) { - throw new Error('Missing Pinecone index name: set PINECONE_INDEX_NAME or pass --index-name.'); - } + const indexName = + trimOptional(overrides.indexName ?? env['PINECONE_INDEX_NAME']) ?? DEFAULT_INDEX_NAME; const sparseIndexName = trimOptional(overrides.sparseIndexName ?? env['PINECONE_SPARSE_INDEX_NAME']) ?? diff --git a/src/core/index.ts b/src/core/index.ts index 5501de8..5b4e6af 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -23,7 +23,7 @@ export { hasUrlGenerator, } from './server/url-registry.js'; export type { UrlGenerationResult, UrlGenerator, UrlGeneratorFn } from './server/url-registry.js'; -export { resolveConfig, DEFAULT_RERANK_MODEL } from './config.js'; +export { resolveConfig, DEFAULT_INDEX_NAME, DEFAULT_RERANK_MODEL } from './config.js'; export type { ServerConfig, LogLevel, LogFormat, ConfigOverrides } from './config.js'; export { PineconeClient } from './pinecone-client.js'; export type { diff --git a/src/types.ts b/src/types.ts index 28fd8b0..8cfe2a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,8 @@ export type PineconeMetadataValue = string | number | boolean | string[]; /** * Configuration for `new PineconeClient(config)`. * - * `apiKey` and `indexName` are required. Values are expected to come from + * `apiKey` is required; `indexName` comes from {@link resolveConfig} (env or default). + * Values are expected to come from * {@link resolveConfig} / CLI or an equivalent resolved object — `PineconeClient` * does not read `process.env` directly. */