From 998ce80da59fe28d9ad59ee355d654979daf4bf6 Mon Sep 17 00:00:00 2001 From: zho Date: Fri, 15 May 2026 02:24:24 +0800 Subject: [PATCH 1/3] exported url generator --- CHANGELOG.md | 1 + README.md | 28 +++++++++++++++++++++ src/server.ts | 9 +++++-- src/server/url-generation.test.ts | 42 +++++++++++++++++++++++++++++-- src/server/url-generation.ts | 41 ++++++++++++++++++++++++++---- 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fade38e..0e88b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### 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. - `.coderabbit.yaml` sets the pre-merge **docstring coverage** threshold to **79%** (default **80%**) so marginal documentation-only gaps do not block merges; adjust upward as coverage improves. - `registerBuiltinUrlGenerators()` for built-in URL generators; `setupServer()` invokes it so CLI/library parity stays default. - Discriminated result type for `listNamespacesFromKeywordIndex()` (`KeywordIndexNamespacesResult`). diff --git a/README.md b/README.md index 33a4947..b73e7a2 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,34 @@ Run `pinecone-read-only-mcp --help` for CLI equivalents (`--cache-ttl-seconds`, 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. +### 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. + +```ts +import { + registerUrlGenerator, + setupServer, + type UrlGenerationResult, + type UrlGeneratorFn, +} from '@will-cppa/pinecone-read-only-mcp'; + +const server = await setupServer(config); + +const myDocs: UrlGeneratorFn = (metadata): UrlGenerationResult => { + 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' }; +}; + +registerUrlGenerator('product-docs', myDocs); +``` + +A fuller embedding sample lives in [examples/custom-url-generator.ts](examples/custom-url-generator.ts). + ### Claude Desktop Configuration Add to your `claude_desktop_config.json`: diff --git a/src/server.ts b/src/server.ts index ca24c86..4950ca8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,7 +9,7 @@ * - {@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. + * - {@link registerUrlGenerator} / {@link unregisterUrlGenerator} — extend URL synthesis (`UrlGeneratorFn`). * - 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`. * @@ -46,7 +46,12 @@ export { hasUrlGenerator, registerBuiltinUrlGenerators, } from './server/url-generation.js'; -export type { UrlGenerationResult, UrlGenerator } 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'; diff --git a/src/server/url-generation.test.ts b/src/server/url-generation.test.ts index 716df20..c6269f9 100644 --- a/src/server/url-generation.test.ts +++ b/src/server/url-generation.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it, beforeAll } from 'vitest'; -import { generateUrlForNamespace, registerBuiltinUrlGenerators } from './url-generation.js'; +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(); @@ -105,3 +111,35 @@ describe('generateUrlForNamespace', () => { 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 index b4e20a1..077ab88 100644 --- a/src/server/url-generation.ts +++ b/src/server/url-generation.ts @@ -28,8 +28,14 @@ export type UrlGenerationResult = { */ 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(); +const urlGenerators = new Map(); /** Return a trimmed non-empty string or null for empty/missing values. */ function asString(value: unknown): string | null { @@ -94,12 +100,37 @@ function generatorSlackCpplang(metadata: Record): UrlGeneration 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`). Idempotent. + * 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(): void { +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); @@ -110,7 +141,7 @@ export function registerBuiltinUrlGenerators(): void { * 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. + * @param generator function that turns a record's metadata into a URL ({@link UrlGeneratorFn}). * * @example * ```ts @@ -124,7 +155,7 @@ export function registerBuiltinUrlGenerators(): void { * }); * ``` */ -export function registerUrlGenerator(namespace: string, generator: UrlGenerator): void { +export function registerUrlGenerator(namespace: string, generator: UrlGeneratorFn): void { urlGenerators.set(namespace, generator); } From 429face4ac8c0436bc9df6edea146b67196561e7 Mon Sep 17 00:00:00 2001 From: zho Date: Fri, 15 May 2026 02:31:02 +0800 Subject: [PATCH 2/3] fixed type errors --- src/server/url-generation.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/url-generation.ts b/src/server/url-generation.ts index 077ab88..2c7c837 100644 --- a/src/server/url-generation.ts +++ b/src/server/url-generation.ts @@ -122,9 +122,7 @@ export type RegisterBuiltinUrlGeneratorsOptions = { * 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 { +export function registerBuiltinUrlGenerators(options?: RegisterBuiltinUrlGeneratorsOptions): void { if (options?.reinstallBuiltins) { urlGenerators.set('mailing', generatorMailing); urlGenerators.set('slack-Cpplang', generatorSlackCpplang); From 1b07975ca7fa441d809fd72d85f8068a007fc231 Mon Sep 17 00:00:00 2001 From: zho Date: Fri, 15 May 2026 02:34:54 +0800 Subject: [PATCH 3/3] addressed ai reivews --- src/server/url-generation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/server/url-generation.ts b/src/server/url-generation.ts index 2c7c837..8acbd11 100644 --- a/src/server/url-generation.ts +++ b/src/server/url-generation.ts @@ -154,7 +154,14 @@ export function registerBuiltinUrlGenerators(options?: RegisterBuiltinUrlGenerat * ``` */ export function registerUrlGenerator(namespace: string, generator: UrlGeneratorFn): void { - urlGenerators.set(namespace, generator); + 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. */