Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
9 changes: 7 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand Down Expand Up @@ -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';
Expand Down
42 changes: 40 additions & 2 deletions src/server/url-generation.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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');
});
});
41 changes: 36 additions & 5 deletions src/server/url-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ export type UrlGenerationResult = {
*/
export type UrlGenerator = (metadata: Record<string, unknown>) => 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<string, UrlGenerator>();
const urlGenerators = new Map<string, UrlGeneratorFn>();

/** Return a trimmed non-empty string or null for empty/missing values. */
function asString(value: unknown): string | null {
Expand Down Expand Up @@ -94,12 +100,37 @@ function generatorSlackCpplang(metadata: Record<string, unknown>): 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);
Expand All @@ -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
Expand All @@ -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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
Loading