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
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ docs/

# Testing
coverage/
dist/__tests__/

# Logs
*.log
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release

## [Unreleased]

### Changed

- **Library:** `resolveConfig()` returns `CoreServerConfig`; `resolveAllianceConfig()` returns `AllianceServerConfig`. `setupCoreServer` / `setupAllianceServer` accept only their respective branded config and context types (`CoreServerContext` / `AllianceServerContext`). `ServerConfig` remains an alias for `ServerConfigBase` on read paths (`ctx.getConfig()`). See [MIGRATION.md](docs/MIGRATION.md#unreleased-branded-serverconfig-types).

## [0.3.0] - 2026-06-23

### Added
Expand Down
31 changes: 31 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,37 @@ Core `resolveConfig` throws `Missing Pinecone index name: …` when the index is

---

## Unreleased: Branded ServerConfig types

**Rationale:** `resolveConfig()` and `resolveAllianceConfig()` produced structurally identical configs. Passing Alliance-resolved config or context into `setupCoreServer()` compiled but silently changed suggest-flow gate behavior (and related defaults).

**Who is affected:** Library embedders typing resolver returns or setup arguments as `ServerConfig` instead of the branded types.

**Migration:**

Use branded resolver outputs with matching setup entry points and context factories:

```typescript
import { createServer, resolveConfig, setupCoreServer } from '@will-cppa/pinecone-read-only-mcp';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

const coreCfg = resolveConfig({ apiKey, indexName });
await setupCoreServer({ context: createServer(coreCfg) });

const allianceCfg = resolveAllianceConfig({ apiKey });
await setupAllianceServer({ context: createServer(allianceCfg) });
```

**Compile-time errors (intentional):**

- `setupCoreServer(allianceCfg)` — Alliance config on core setup
- `setupCoreServer({ context: createServer(allianceCfg) })` — Alliance context on core setup
- `setupAllianceServer(coreCfg)` and `setupAllianceServer({ context: createServer(coreCfg) })` — symmetric misuse

**Unchanged:** `ServerConfig` / `ServerConfigBase` remain valid for reading fields from `ctx.getConfig()` and other read-only config paths.

---

## Migrating to v0.2.0

### Namespace trimming and suggest-flow
Expand Down
5 changes: 2 additions & 3 deletions examples/alliance/custom-url-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
import {
createServer,
PineconeClient,
resolveConfig,
type UrlGenerationResult,
} from '@will-cppa/pinecone-read-only-mcp';
import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

async function main(): Promise<void> {
const apiKey = process.env['PINECONE_API_KEY']?.trim();
Expand All @@ -31,7 +30,7 @@ async function main(): Promise<void> {
);
return;
}
const config = resolveConfig({ apiKey, indexName });
const config = resolveAllianceConfig({ apiKey, indexName });

const ctx = createServer(config);
ctx.setClient(
Expand Down
5 changes: 2 additions & 3 deletions examples/alliance/guided-query-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
import {
createServer,
PineconeClient,
resolveConfig,
} from '@will-cppa/pinecone-read-only-mcp';
import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

async function main(): Promise<void> {
const apiKey = process.env['PINECONE_API_KEY']?.trim();
Expand All @@ -33,7 +32,7 @@ async function main(): Promise<void> {
return;
}

const config = resolveConfig({ apiKey, indexName });
const config = resolveAllianceConfig({ apiKey, indexName });
const ctx = createServer(config);
ctx.setClient(
new PineconeClient({
Expand Down
5 changes: 2 additions & 3 deletions examples/alliance/suggest-flow-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
import {
createServer,
PineconeClient,
resolveConfig,
} from '@will-cppa/pinecone-read-only-mcp';
import { setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';

async function main(): Promise<void> {
const apiKey = process.env['PINECONE_API_KEY']?.trim();
Expand All @@ -34,7 +33,7 @@ async function main(): Promise<void> {
return;
}

const config = resolveConfig({ apiKey, indexName });
const config = resolveAllianceConfig({ apiKey, indexName });
const ctx = createServer(config);
ctx.setClient(
new PineconeClient({
Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/config-branding.compile-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Compile-time assertions for ServerConfig branding (Issue #172).
*
* Included in `tsc` / `npm run typecheck` (not `*.test.ts`). Misuse cases use
* `@ts-expect-error` so brand regressions fail CI when the guard stops working.
*/

import { resolveAllianceConfig } from '../alliance/config.js';
import { resolveConfig } from '../core/config.js';
import { createServer } from '../core/server/server-context.js';
import { setupCoreServer } from '../core/setup.js';
import { setupAllianceServer } from '../alliance/setup.js';

const coreCfg = resolveConfig({ apiKey: 'k', indexName: 'idx' });
const allianceCfg = resolveAllianceConfig({ apiKey: 'k' });

void setupCoreServer(coreCfg);
void setupAllianceServer(allianceCfg);

// @ts-expect-error Alliance config must not be passed to core setup
void setupCoreServer(allianceCfg);

// @ts-expect-error Core config must not be passed to Alliance setup
void setupAllianceServer(coreCfg);

const allianceCtx = createServer(allianceCfg);
// @ts-expect-error Alliance context must not be passed to core setup
void setupCoreServer({ context: allianceCtx });

const coreCtx = createServer(coreCfg);
// @ts-expect-error Core context must not be passed to Alliance setup
void setupAllianceServer({ context: coreCtx });
11 changes: 7 additions & 4 deletions src/alliance/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

import {
asBool,
brandAllianceConfig,
resolveConfig,
trimOptional,
type AllianceServerConfig,
type ConfigOverrides,
type ServerConfig,
} from '../core/config.js';

/** C++ Alliance default dense index when env/CLI omit `PINECONE_INDEX_NAME`. */
Expand All @@ -20,7 +21,7 @@ export const ALLIANCE_DEFAULT_RERANK_MODEL = 'bge-reranker-v2-m3';
export const DEFAULT_ALLIANCE_RERANK_MODEL = ALLIANCE_DEFAULT_RERANK_MODEL;

/**
* Build {@link ServerConfig} for Alliance CLI and `setupAllianceServer`.
* Build {@link AllianceServerConfig} for Alliance CLI and `setupAllianceServer`.
* Fills index and rerank from Alliance defaults when unset, then calls core `resolveConfig`.
*
* **Suggest-flow gate default:** After core resolution, `disableSuggestFlow` is overridden to
Expand All @@ -37,7 +38,7 @@ export const DEFAULT_ALLIANCE_RERANK_MODEL = ALLIANCE_DEFAULT_RERANK_MODEL;
export function resolveAllianceConfig(
overrides: ConfigOverrides = {},
env: NodeJS.ProcessEnv = process.env
): ServerConfig {
): AllianceServerConfig {
const indexName =
trimOptional(overrides.indexName) ??
trimOptional(env['PINECONE_INDEX_NAME']) ??
Expand All @@ -49,5 +50,7 @@ export function resolveAllianceConfig(
const cfg = resolveConfig({ ...overrides, indexName, rerankModel }, env);
const disableSuggestFlow =
overrides.disableSuggestFlow ?? asBool(env['PINECONE_DISABLE_SUGGEST_FLOW'], false);
return { ...cfg, disableSuggestFlow };
return brandAllianceConfig({ ...cfg, disableSuggestFlow });
}

export type { AllianceServerConfig } from '../core/config.js';
2 changes: 2 additions & 0 deletions src/alliance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export {
DEFAULT_ALLIANCE_RERANK_MODEL,
resolveAllianceConfig,
} from './config.js';
export type { AllianceServerConfig } from './config.js';
export type { AllianceServerContext } from '../core/server/server-context.js';
export { setupAllianceServer, type SetupAllianceServerOptions } from './setup.js';
export {
registerBuiltinUrlGenerators,
Expand Down
76 changes: 52 additions & 24 deletions src/alliance/setup.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import { ALLIANCE_SERVER_INSTRUCTIONS } from '../constants.js';
import type { ServerConfig } from '../core/config.js';
import { resolveDefaultServerContext, type ServerContext } from '../core/server/server-context.js';
import type { AllianceServerConfig, ServerConfigBase } from '../core/config.js';
import { getServerConfigLineage } from '../core/config.js';
import { createServer, type AllianceServerContext } from '../core/server/server-context.js';
import { resolveAllianceConfig } from './config.js';
import { setupCoreServer, type ServerHandle, type SetupCoreServerOptions } from '../core/setup.js';
import { setupCoreServerOnContext, type ServerHandle } from '../core/setup.js';
import { registerBuiltinUrlGenerators } from './url-builtins.js';
import { registerSuggestQueryParamsTool } from './tools/suggest-query-params-tool.js';

/**
* Options for {@link setupAllianceServer}.
*/
export type SetupAllianceServerOptions = {
config?: ServerConfig;
context?: ServerContext;
config?: AllianceServerConfig;
context?: AllianceServerContext;
/** 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 isServerConfig(value: unknown): value is AllianceServerConfig {
if (typeof value !== 'object' || value === null) {
return false;
}
const base = value as ServerConfigBase;
if (typeof base.apiKey !== 'string' || typeof base.indexName !== 'string') {
return false;
}
return getServerConfigLineage(base) === 'alliance';
}

function assertAllianceServerConfig(config: ServerConfigBase): AllianceServerConfig {
if (getServerConfigLineage(config) !== 'alliance') {
throw new TypeError(
'Expected AllianceServerConfig. Use setupCoreServer for core-branded config.'
);
}
return config as AllianceServerConfig;
}

function isSetupAllianceServerOptions(value: unknown): value is SetupAllianceServerOptions {
Expand All @@ -38,7 +50,7 @@ function isSetupAllianceServerOptions(value: unknown): value is SetupAllianceSer
}

function normalizeSetupAllianceArgs(
configOrOptions?: ServerConfig | SetupAllianceServerOptions,
configOrOptions?: AllianceServerConfig | SetupAllianceServerOptions,
legacyOptions?: Pick<SetupAllianceServerOptions, 'instructions'>
): SetupAllianceServerOptions {
if (configOrOptions === undefined) {
Expand All @@ -50,7 +62,9 @@ function normalizeSetupAllianceArgs(
if (isSetupAllianceServerOptions(configOrOptions)) {
return { ...configOrOptions, ...legacyOptions };
}
throw new TypeError('configOrOptions must be a ServerConfig or SetupAllianceServerOptions');
throw new TypeError(
'configOrOptions must be an AllianceServerConfig or SetupAllianceServerOptions'
);
}

/**
Expand All @@ -60,29 +74,43 @@ function normalizeSetupAllianceArgs(
* When `config` is omitted, resolves env via {@link resolveAllianceConfig} (Alliance index/rerank defaults when unset).
*/
export async function setupAllianceServer(
configOrOptions?: ServerConfig | SetupAllianceServerOptions,
configOrOptions?: AllianceServerConfig | SetupAllianceServerOptions,
legacyOptions?: Pick<SetupAllianceServerOptions, 'instructions'>
): Promise<ServerHandle> {
const opts = normalizeSetupAllianceArgs(configOrOptions, legacyOptions);
const instructions = opts.instructions ?? ALLIANCE_SERVER_INSTRUCTIONS;

if (opts.config) {
assertAllianceServerConfig(opts.config);
}

let server: ServerHandle;
let resolvedCtx: ServerContext;
let resolvedCtx: AllianceServerContext;

if (opts.context) {
resolvedCtx = opts.context;
const coreOpts: SetupCoreServerOptions = { context: resolvedCtx, instructions };
if (opts.config !== undefined) {
coreOpts.config = opts.config;
if (resolvedCtx.hasInjectedClient()) {
throw new Error(
'Passing both config and context clears an injected Pinecone client. ' +
'Omit config when reusing a pre-configured context, or call setClient() after setup.'
);
}
resolvedCtx.setConfig(opts.config);
} else if (!resolvedCtx.hasConfig()) {
resolvedCtx.setConfig(resolveAllianceConfig({}));
} else {
const stored = resolvedCtx.getConfigIfSet();
if (stored) {
assertAllianceServerConfig(stored);
}
}
server = await setupCoreServer(coreOpts);
server = await setupCoreServerOnContext(resolvedCtx, instructions);
} else {
const config = opts.config ?? resolveAllianceConfig({});
server = await setupCoreServer({
config: opts.config ?? config,
instructions,
});
resolvedCtx = resolveDefaultServerContext();
const ctx = createServer(config);
server = await setupCoreServerOnContext(ctx, instructions);
resolvedCtx = ctx;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

registerBuiltinUrlGenerators(resolvedCtx);
Expand Down
Loading
Loading