Skip to content

Commit 66fe34a

Browse files
jonathanMLDevzho
andauthored
exported url generator (cppalliance#87)
* exported url generator * fixed type errors * addressed ai reivews --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 4b78587 commit 66fe34a

5 files changed

Lines changed: 118 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release
1010

1111
### Added
1212

13+
- `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.
1314
- Zod `toolErrorSchema` and exported types `ToolError` / `ToolErrorCode` for parsing MCP tool failures; all tools now return this JSON shape in the text content when `isError` is true.
1415
- `validateMetadataFilterDetailed()` returns `{ message, field }` for invalid filters; `validateMetadataFilter()` remains a string-only wrapper for backward compatibility.
1516
- `.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.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,34 @@ Run `pinecone-read-only-mcp --help` for CLI equivalents (`--cache-ttl-seconds`,
102102

103103
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.
104104

105+
### Custom URL generators
106+
107+
Namespaces other than `mailing` and `slack-Cpplang` (or different URL rules for any namespace) can use programmatic registration — no fork required.
108+
109+
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.
110+
111+
```ts
112+
import {
113+
registerUrlGenerator,
114+
setupServer,
115+
type UrlGenerationResult,
116+
type UrlGeneratorFn,
117+
} from '@will-cppa/pinecone-read-only-mcp';
118+
119+
const server = await setupServer(config);
120+
121+
const myDocs: UrlGeneratorFn = (metadata): UrlGenerationResult => {
122+
const id = typeof metadata.doc_id === 'string' ? metadata.doc_id : null;
123+
return id
124+
? { url: `https://docs.example.com/${id}`, method: 'generated.custom' }
125+
: { url: null, method: 'unavailable', reason: 'doc_id missing' };
126+
};
127+
128+
registerUrlGenerator('product-docs', myDocs);
129+
```
130+
131+
A fuller embedding sample lives in [examples/custom-url-generator.ts](examples/custom-url-generator.ts).
132+
105133
### Claude Desktop Configuration
106134

107135
Add to your `claude_desktop_config.json`:

src/server.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* - {@link PineconeClient} — hybrid search, count, namespace listing, etc.
1010
* - {@link resolveConfig} — merge CLI-style overrides with `process.env`.
1111
* - {@link setPineconeClient} — inject a client instance before `setupServer()`.
12-
* - {@link registerUrlGenerator} / {@link unregisterUrlGenerator} — extend URL synthesis.
12+
* - {@link registerUrlGenerator} / {@link unregisterUrlGenerator} — extend URL synthesis (`UrlGeneratorFn`).
1313
* - {@link toolErrorSchema} / {@link ToolError} — parse MCP tool failures (`isError: true` JSON bodies).
1414
* - Built-in `mailing` / `slack-Cpplang` URL generators are registered from {@link setupServer}
1515
* via {@link registerBuiltinUrlGenerators}; call it yourself if you use the library without `setupServer`.
@@ -53,7 +53,12 @@ export {
5353
hasUrlGenerator,
5454
registerBuiltinUrlGenerators,
5555
} from './server/url-generation.js';
56-
export type { UrlGenerationResult, UrlGenerator } from './server/url-generation.js';
56+
export type {
57+
UrlGenerationResult,
58+
UrlGenerator,
59+
UrlGeneratorFn,
60+
RegisterBuiltinUrlGeneratorsOptions,
61+
} from './server/url-generation.js';
5762
/** Build {@link ServerConfig} from CLI overrides + environment variables. */
5863
export { resolveConfig } from './config.js';
5964
export type { ServerConfig, LogLevel, LogFormat, ConfigOverrides } from './config.js';

src/server/url-generation.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { describe, expect, it, beforeAll } from 'vitest';
2-
import { generateUrlForNamespace, registerBuiltinUrlGenerators } from './url-generation.js';
1+
import { describe, expect, it, beforeAll, afterEach } from 'vitest';
2+
import type { UrlGeneratorFn } from './url-generation.js';
3+
import {
4+
generateUrlForNamespace,
5+
registerBuiltinUrlGenerators,
6+
registerUrlGenerator,
7+
unregisterUrlGenerator,
8+
} from './url-generation.js';
39

410
beforeAll(() => {
511
registerBuiltinUrlGenerators();
@@ -105,3 +111,35 @@ describe('generateUrlForNamespace', () => {
105111
expect(r.method).toBe('unavailable');
106112
});
107113
});
114+
115+
describe('registerUrlGenerator', () => {
116+
const customNs = 'acme-docs';
117+
118+
afterEach(() => {
119+
unregisterUrlGenerator(customNs);
120+
registerBuiltinUrlGenerators({ reinstallBuiltins: true });
121+
});
122+
123+
it('registers a custom generator for a new namespace', () => {
124+
const fn: UrlGeneratorFn = () => ({
125+
url: 'https://example.com/doc/1',
126+
method: 'generated.custom',
127+
});
128+
registerUrlGenerator(customNs, fn);
129+
const r = generateUrlForNamespace(customNs, {});
130+
expect(r.url).toBe('https://example.com/doc/1');
131+
expect(r.method).toBe('generated.custom');
132+
});
133+
134+
it('allows a custom generator to override the mailing built-in', () => {
135+
registerUrlGenerator('mailing', () => ({
136+
url: 'https://override.example/mailing',
137+
method: 'generated.custom',
138+
}));
139+
const r = generateUrlForNamespace('mailing', {
140+
doc_id: 'boost-announce@lists.boost.org/message/O5VYCDZADVDHK5Z5LAYJBHMDOAFQL7P6',
141+
});
142+
expect(r.url).toBe('https://override.example/mailing');
143+
expect(r.method).toBe('generated.custom');
144+
});
145+
});

src/server/url-generation.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,14 @@ export type UrlGenerationResult = {
2828
*/
2929
export type UrlGenerator = (metadata: Record<string, unknown>) => UrlGenerationResult;
3030

31+
/**
32+
* Alias for {@link UrlGenerator} (issue / API naming: `UrlGeneratorFn`).
33+
* Use either type when implementing custom URL synthesis.
34+
*/
35+
export type UrlGeneratorFn = UrlGenerator;
36+
3137
/** Registry of namespace -> URL generator. Built-ins register via {@link registerBuiltinUrlGenerators}. */
32-
const urlGenerators = new Map<string, UrlGenerator>();
38+
const urlGenerators = new Map<string, UrlGeneratorFn>();
3339

3440
/** Return a trimmed non-empty string or null for empty/missing values. */
3541
function asString(value: unknown): string | null {
@@ -94,12 +100,35 @@ function generatorSlackCpplang(metadata: Record<string, unknown>): UrlGeneration
94100

95101
let builtinGeneratorsRegistered = false;
96102

103+
/** Options for {@link registerBuiltinUrlGenerators}. */
104+
export type RegisterBuiltinUrlGeneratorsOptions = {
105+
/**
106+
* When `true`, re-applies the built-in `mailing` and `slack-Cpplang` generators
107+
* even if they were already registered or were replaced by {@link registerUrlGenerator}.
108+
* Use to restore defaults after an override (e.g. in tests).
109+
*/
110+
reinstallBuiltins?: boolean;
111+
};
112+
97113
/**
98-
* Register built-in generators (`mailing`, `slack-Cpplang`). Idempotent.
114+
* Register built-in generators (`mailing`, `slack-Cpplang`).
115+
*
116+
* With no options (or `reinstallBuiltins` omitted / `false`), the call is idempotent:
117+
* only the first invocation in the process installs the two built-ins.
118+
*
119+
* With `{ reinstallBuiltins: true }`, always resets `mailing` and `slack-Cpplang` to
120+
* the library implementations (does not remove other custom namespaces).
121+
*
99122
* Invoked from {@link setupServer} so embedders get the same defaults as the CLI;
100123
* pure library use without calling `setupServer` should register explicitly if needed.
101124
*/
102-
export function registerBuiltinUrlGenerators(): void {
125+
export function registerBuiltinUrlGenerators(options?: RegisterBuiltinUrlGeneratorsOptions): void {
126+
if (options?.reinstallBuiltins) {
127+
urlGenerators.set('mailing', generatorMailing);
128+
urlGenerators.set('slack-Cpplang', generatorSlackCpplang);
129+
builtinGeneratorsRegistered = true;
130+
return;
131+
}
103132
if (builtinGeneratorsRegistered) return;
104133
urlGenerators.set('mailing', generatorMailing);
105134
urlGenerators.set('slack-Cpplang', generatorSlackCpplang);
@@ -110,7 +139,7 @@ export function registerBuiltinUrlGenerators(): void {
110139
* Register a URL generator for a namespace, replacing any existing entry.
111140
*
112141
* @param namespace exact namespace name (matches the value returned by `list_namespaces`).
113-
* @param generator function that turns a record's metadata into a URL.
142+
* @param generator function that turns a record's metadata into a URL ({@link UrlGeneratorFn}).
114143
*
115144
* @example
116145
* ```ts
@@ -124,8 +153,15 @@ export function registerBuiltinUrlGenerators(): void {
124153
* });
125154
* ```
126155
*/
127-
export function registerUrlGenerator(namespace: string, generator: UrlGenerator): void {
128-
urlGenerators.set(namespace, generator);
156+
export function registerUrlGenerator(namespace: string, generator: UrlGeneratorFn): void {
157+
const normalizedNamespace = namespace.trim();
158+
if (normalizedNamespace.length === 0) {
159+
throw new TypeError('namespace must be a non-empty string');
160+
}
161+
if (typeof generator !== 'function') {
162+
throw new TypeError('generator must be a function');
163+
}
164+
urlGenerators.set(normalizedNamespace, generator);
129165
}
130166

131167
/** Remove a namespace's URL generator. Returns true if a generator was removed. */

0 commit comments

Comments
 (0)