Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ To try the server on **your own** Pinecone project (free tier, no Alliance index
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).
- **`src/alliance/`** — C++ Alliance app tools (`suggest_query_params`, `guided_query`, Boost/Slack URL builtins). Import from `@will-cppa/pinecone-read-only-mcp/alliance`; embed via `resolveAllianceConfig` → `createServer` / `setClient` → `setupAllianceServer({ context: ctx })` (see [Library embedding](#library-embedding)).

## Configuration

Expand All @@ -125,16 +125,29 @@ 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 `setupAllianceServer` behind a multi-tenant HTTP transport, isolate those structures per session yourself or treat the suggest-flow guard as best-effort only.
Each **`ServerContext`** owns its own suggest-flow gate, namespaces cache, URL generator registry, and config. **Stdio MCP (one client per Node process)** typically uses one context. For **multi-tenant HTTP** embedding, create one `ServerContext` per session and pass it explicitly to `setupAllianceServer({ context: ctx })` or `setupCoreServer({ context: ctx })`.

Pass `config` at setup only when the context is not yet configured; after `createServer` + `setClient`, pass `{ context: ctx }` only.

Legacy module getters (`setPineconeClient`, `registerUrlGenerator`, etc.) still delegate to a process-default context when you omit `context` at setup.

### Library embedding

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 (instance-first):** create a `ServerContext`, inject the client, and pass it to setup:

- **Generic bridge only:** `import { setupCoreServer, teardownServer, ... } from '@will-cppa/pinecone-read-only-mcp'`
- **Generic bridge only:** `import { createServer, setupCoreServer, teardownServer, ... } from '@will-cppa/pinecone-read-only-mcp'`
- **Full Alliance surface (CLI parity):** `import { setupAllianceServer, resolveAllianceConfig } from '@will-cppa/pinecone-read-only-mcp/alliance'`

For the **generic bridge only**, see [examples/quickstart/mcp-demo.ts](examples/quickstart/mcp-demo.ts) (`setupCoreServer` + `resolveConfig` with required `indexName`; suggest-flow gate **off** by default). For the **full Alliance surface**, use `resolveAllianceConfig({ apiKey, ... })` (Alliance index/rerank defaults when omitted, suggest-flow gate **on** by default, same as the CLI) → `setPineconeClient(new PineconeClient(...))` → `await setupAllianceServer(config)` → connect one MCP transport. See [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate).
```ts
const config = resolveAllianceConfig({ apiKey: '...' });
const ctx = createServer(config);
ctx.setClient(new PineconeClient({ /* ... */ }));
const server = await setupAllianceServer({ context: ctx });
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Use **`await using server = await setupAllianceServer({ context: ctx })`** for automatic teardown, or call **`ctx.teardown()`** when done. For legacy single-server flows that rely on the process-default context, **`teardownServer()`** resets that default before re-initializing.

For the **generic bridge only**, see [examples/quickstart/mcp-demo.ts](examples/quickstart/mcp-demo.ts). For the **full Alliance surface**, see [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) and [docs/TOOLS.md](docs/TOOLS.md#suggest-flow-gate).

### Custom URL generators

Expand All @@ -144,24 +157,25 @@ Import `registerUrlGenerator` and types `UrlGeneratorFn` / `UrlGenerationResult`

```ts
import {
createServer,
PineconeClient,
registerUrlGenerator,
setPineconeClient,
type UrlGenerationResult,
type UrlGeneratorFn,
} from '@will-cppa/pinecone-read-only-mcp';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

const config = resolveAllianceConfig({ apiKey: '...' }); // optional: indexName, rerankModel
setPineconeClient(
const ctx = createServer(config);
ctx.setClient(
new PineconeClient({
apiKey: config.apiKey,
indexName: config.indexName,
sparseIndexName: config.sparseIndexName,
rerankModel: config.rerankModel,
})
);
const server = await setupAllianceServer(config);
const server = await setupAllianceServer({ context: ctx });

const myDocs: UrlGeneratorFn = (metadata): UrlGenerationResult => {
const id = typeof metadata.doc_id === 'string' ? metadata.doc_id : null;
Expand Down
10 changes: 6 additions & 4 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Configuration is built from **CLI flags** (when using the binary), **environment

**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)`.
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 follow the [Library embedding](#library-embedding) flow: `resolveAllianceConfig` → `createServer` / `setClient` → `setupAllianceServer({ context: ctx })`.

### Core vs Alliance resolvers

Expand Down Expand Up @@ -63,9 +63,11 @@ C++ Alliance deployers can copy [examples/alliance/.env.example](../examples/all

## Library embedding

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.
1. Build `ServerConfig` with `resolveConfig({ apiKey: '...', indexName: '...', ... })` or `resolveAllianceConfig(...)` for the full tool surface.
2. `const ctx = createServer(config)` then `ctx.setClient(new PineconeClient({ ... }))` (mirrors `src/index.ts`).
3. `await setupAllianceServer({ context: ctx })` (or `setupCoreServer({ context: ctx })` for generic tools only) then connect an MCP transport.

Pass `config` at setup only when the context is not yet configured; after `createServer` + `setClient`, pass `{ context: ctx }` only.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
See [README deployment model](../README.md#deployment-model), [examples/quickstart/README.md](../examples/quickstart/README.md) (generic), and [examples/alliance/library-embedding-demo.ts](../examples/alliance/library-embedding-demo.ts) (Alliance surface).

Expand Down
23 changes: 19 additions & 4 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const server = await setupAllianceServer(config);

Module-level helpers (`getPineconeClient`, `registerUrlGenerator`, `requireSuggested`, etc.) continue to work; they delegate to a process-default context.

**New (opt-in instance path):**
**New (recommended — phase 4 explicit context at setup):**

```ts
import { createServer, PineconeClient } from '@will-cppa/pinecone-read-only-mcp';
Expand All @@ -40,7 +40,7 @@ import {
} from '@will-cppa/pinecone-read-only-mcp/alliance';

const config = resolveAllianceConfig({ apiKey: process.env.PINECONE_API_KEY! });
const ctx = createServer(config); // installs process-default + returns instance
const ctx = createServer(config);
ctx.setClient(
new PineconeClient({
apiKey: config.apiKey,
Expand All @@ -51,17 +51,32 @@ ctx.setClient(
requestTimeoutMs: config.requestTimeoutMs,
})
);
const server = await setupAllianceServer(config); // uses process-default ctx for migrated tools
const server = await setupAllianceServer({ context: ctx });
```

Pass `config` at setup only when the context is not yet configured; after `createServer` + `setClient`, pass `{ context: ctx }` only.

**Core-only setup** (seven tools, no Alliance builtins):

```ts
import { createServer, PineconeClient, resolveConfig, setupCoreServer } from '@will-cppa/pinecone-read-only-mcp';

const config = resolveConfig({ apiKey: '...', indexName: 'my-index' });
const ctx = createServer(config);
ctx.setClient(new PineconeClient({ /* ... */ }));
const server = await setupCoreServer({ context: ctx });
```

**Multi-instance:** run multiple `ServerContext` instances in one process by passing a distinct `context` to each setup call. Use `await using` on the returned `ServerHandle` or `ctx.teardown()` per session. Legacy `teardownServer()` resets only the process-default context.

For custom tool wiring, pass `ctx` to migrated registrars:

```ts
import { registerQueryTool, registerCountTool, registerListNamespacesTool } from '…'; // internal today
registerQueryTool(server, ctx);
```

**Later (future minors/major):** Legacy module getters will be marked `### Deprecated` per [deprecation-policy.md](./deprecation-policy.md). Multi-tenant HTTP embedders should use one `ServerContext` per session rather than sharing process-global state.
**Later (future minors/major):** Legacy module getters will be marked `### Deprecated` per [deprecation-policy.md](./deprecation-policy.md).

See also [deprecation-policy.md § Future instance APIs](./deprecation-policy.md#future-instance-apis-servercontext).

Expand Down
28 changes: 18 additions & 10 deletions examples/alliance/library-embedding-demo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
/**
* Library embedding: build the MCP server from a Node script (not the CLI).
*
* Pattern (mirrors `src/index.ts`):
* Instance-first pattern (mirrors `src/index.ts`):
* 1. `resolveAllianceConfig({ apiKey, indexName, ... })` — Alliance index/rerank defaults when unset.
* 2. `new PineconeClient({ ... })` + `setPineconeClient(client)` (legacy), or `createServer(config)` + `ctx.setClient(...)`.
* 3. `await setupAllianceServer(config)` then `server.connect(transport)`.
* 2. `createServer(config)` `ctx.setClient(new PineconeClient({ ... }))`.
* 3. `await setupAllianceServer({ context: ctx })` then `server.connect(transport)`.
*
* **Single process:** `setupAllianceServer` registers tools against process-global
* singletons (suggest-flow state, namespaces cache, URL registry, active config).
* A second setup call throws — call `teardownServer()` first to re-initialize
* (tests). For isolated tenants in production, prefer one server per Node process.
* Pass `config` at setup only when the context is not yet configured; after
* `createServer` + `setClient`, pass `{ context: ctx }` only.
*
* **Multi-instance:** pass a distinct `ServerContext` per tenant/session. Each context
* owns its own suggest-flow state, namespaces cache, and URL registry. Use
* `await using server = await setupAllianceServer({ context: ctx })` for
* automatic teardown, or call `ctx.teardown()` when done.
*
* **Legacy (single process-default server):** `setPineconeClient(client)` then
* `await setupAllianceServer(config)` still works; call `teardownServer()` before
* re-initializing the default context.
*/

import { PineconeClient, setPineconeClient } from '@will-cppa/pinecone-read-only-mcp';
import { createServer, PineconeClient } from '@will-cppa/pinecone-read-only-mcp';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

async function main(): Promise<void> {
Expand All @@ -25,7 +32,8 @@ async function main(): Promise<void> {
}
const config = resolveAllianceConfig({ apiKey });

setPineconeClient(
const ctx = createServer(config);
ctx.setClient(
new PineconeClient({
apiKey: config.apiKey,
indexName: config.indexName,
Expand All @@ -36,7 +44,7 @@ async function main(): Promise<void> {
})
);

const server = await setupAllianceServer(config);
const server = await setupAllianceServer({ context: ctx });
// const transport = new StdioServerTransport();
// await server.connect(transport);
void server;
Expand Down
2 changes: 1 addition & 1 deletion src/alliance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
DEFAULT_ALLIANCE_RERANK_MODEL,
resolveAllianceConfig,
} from './config.js';
export { setupAllianceServer } from './setup.js';
export { setupAllianceServer, type SetupAllianceServerOptions } from './setup.js';
export {
registerBuiltinUrlGenerators,
generatorMailing,
Expand Down
88 changes: 78 additions & 10 deletions src/alliance/setup.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,93 @@
import { ALLIANCE_SERVER_INSTRUCTIONS } from '../constants.js';
import type { ServerConfig } from '../core/config.js';
import { getDefaultServerContext } from '../core/server/server-context.js';
import { getDefaultServerContext, type ServerContext } from '../core/server/server-context.js';
import { resolveAllianceConfig } from './config.js';
import { setupCoreServer, type ServerHandle } from '../core/setup.js';
import { setupCoreServer, type ServerHandle, type SetupCoreServerOptions } from '../core/setup.js';
import { registerBuiltinUrlGenerators } from './url-builtins.js';
import { registerGuidedQueryTool } from './tools/guided-query-tool.js';
import { registerSuggestQueryParamsTool } from './tools/suggest-query-params-tool.js';

/**
* Options for {@link setupAllianceServer}.
*/
export type SetupAllianceServerOptions = {
config?: ServerConfig;
context?: ServerContext;
/** MCP server instructions; defaults to {@link ALLIANCE_SERVER_INSTRUCTIONS}. */
instructions?: string;
};

function isServerConfig(value: unknown): value is ServerConfig {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as ServerConfig).apiKey === 'string' &&
typeof (value as ServerConfig).indexName === 'string'
);
}

function isSetupAllianceServerOptions(value: unknown): value is SetupAllianceServerOptions {
if (typeof value !== 'object' || value === null || isServerConfig(value)) {
return false;
}
for (const key of Object.keys(value as Record<string, unknown>)) {
if (key !== 'config' && key !== 'context' && key !== 'instructions') {
return false;
}
}
return true;
}

function normalizeSetupAllianceArgs(
configOrOptions?: ServerConfig | SetupAllianceServerOptions,
legacyOptions?: Pick<SetupAllianceServerOptions, 'instructions'>
): SetupAllianceServerOptions {
if (configOrOptions === undefined) {
return legacyOptions ?? {};
}
if (isServerConfig(configOrOptions)) {
return { config: configOrOptions, ...legacyOptions };
}
if (isSetupAllianceServerOptions(configOrOptions)) {
return { ...configOrOptions, ...legacyOptions };
}
throw new TypeError('configOrOptions must be a ServerConfig or SetupAllianceServerOptions');
}

/**
* 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 resolveAllianceConfig} (Alliance index/rerank defaults when unset).
*/
export async function setupAllianceServer(config?: ServerConfig): Promise<ServerHandle> {
const server = await setupCoreServer(config ?? resolveAllianceConfig({}), {
instructions: ALLIANCE_SERVER_INSTRUCTIONS,
});
const ctx = getDefaultServerContext();
registerBuiltinUrlGenerators(ctx);
registerSuggestQueryParamsTool(server, ctx);
registerGuidedQueryTool(server, ctx);
export async function setupAllianceServer(
configOrOptions?: ServerConfig | SetupAllianceServerOptions,
legacyOptions?: Pick<SetupAllianceServerOptions, 'instructions'>
): Promise<ServerHandle> {
const opts = normalizeSetupAllianceArgs(configOrOptions, legacyOptions);
const instructions = opts.instructions ?? ALLIANCE_SERVER_INSTRUCTIONS;

let server: ServerHandle;
let resolvedCtx: ServerContext;

if (opts.context) {
resolvedCtx = opts.context;
const coreOpts: SetupCoreServerOptions = { context: resolvedCtx, instructions };
if (opts.config !== undefined) {
coreOpts.config = opts.config;
}
server = await setupCoreServer(coreOpts);
} else {
const config = opts.config ?? resolveAllianceConfig({});
server = await setupCoreServer({
config: opts.config ?? config,
instructions,
});
resolvedCtx = getDefaultServerContext();
}

registerBuiltinUrlGenerators(resolvedCtx);
registerSuggestQueryParamsTool(server, resolvedCtx);
registerGuidedQueryTool(server, resolvedCtx);
return server;
}
7 changes: 6 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ export type {
HybridQueryResult,
HybridLegFailed,
} from '../types.js';
export { setupCoreServer, teardownServer, type ServerHandle } from './setup.js';
export {
setupCoreServer,
teardownServer,
type ServerHandle,
type SetupCoreServerOptions,
} from './setup.js';
4 changes: 4 additions & 0 deletions src/core/server/server-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe('ServerContext', () => {
const client = ctx.getClient();
expect(client).toBeInstanceOf(PineconeClient);
expect(ctx.getClient()).toBe(client);
expect(ctx.hasInjectedClient()).toBe(false);
expect(() => ctx.getClientIfSet()).toThrow(/not initialized/);
});

it('honors externally injected client via setClient and fromClient', () => {
Expand All @@ -31,9 +33,11 @@ describe('ServerContext', () => {
viaSetter.setClient(injected);
expect(viaSetter.getClient()).toBe(injected);
expect(viaSetter.getClientIfSet()).toBe(injected);
expect(viaSetter.hasInjectedClient()).toBe(true);

const viaFactory = ServerContext.fromClient(config, injected);
expect(viaFactory.getClient()).toBe(injected);
expect(viaFactory.hasInjectedClient()).toBe(true);
});

it('createServer installs default context', () => {
Expand Down
Loading
Loading