diff --git a/CHANGELOG.md b/CHANGELOG.md index 604d150..4ec5c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release - **Per-source and per-namespace private config:** optional `description` (source-level) and `namespaces` map (`description` + declared `metadata_schema`) in JSON config files (`PINECONE_CONFIG_FILE` only — not inline `PINECONE_SOURCES`). Declared schemas skip live sampling; stale declared namespaces surface as `config_warnings` in `list_namespaces`. See [CONFIGURATION.md](docs/CONFIGURATION.md#json-config-file). - **`list_namespaces`:** optional per-namespace `schema_source` (`declared` | `sampled`), optional per-namespace `description` (from private config), and top-level `config_warnings` when private config declarations do not match live Pinecone data. +- **Tests:** MCP verification harness ([src/__tests__/mcp-rc-readiness.test.ts](src/__tests__/mcp-rc-readiness.test.ts)) that drives the real server over an in-memory transport with the SDK client (initialize and protocol negotiation, the full registered tool surface, and a round-trip tool call), so a future SDK or MCP protocol bump is verified in one place. Protocol assertions key off the SDK's `LATEST_PROTOCOL_VERSION`, so pinning the RC SDK re-runs the check with no test edits. (#202) ### Changed - **Breaking (`list_sources`):** response shape `sources: string[]` → `sources: { name, description? }[]`. Migration: [MIGRATION.md § Unreleased list_sources](docs/MIGRATION.md#unreleased-list_sources-response-shape). - **Breaking (library API):** `PineconeClient.listNamespacesWithMetadata()` now returns `{ namespaces, warnings }` instead of a bare array. Migration: [MIGRATION.md § Unreleased PineconeClient.listNamespacesWithMetadata](docs/MIGRATION.md#unreleased-pineconeclientlistnamespaceswithmetadata-return-shape). +- **Dependencies:** Bumped the declared `@modelcontextprotocol/sdk` floor from `^1.25.3` to `^1.29.0` to match the resolved version and ready the server for the upcoming MCP RC protocol revision. `McpServer` construction and tool registration are re-verified by the harness above through `server.connect()` over the SDK in-memory transport; the production `StdioServerTransport` wiring in `src/index.ts` is unchanged and was reviewed, not exercised, by the harness. (#202) - **Instructions:** Trimmed operator/install/deploy content (env-var setup, misconfiguration note, Alliance CLI index/rerank defaults, stderr logging config) from `CORE_SERVER_INSTRUCTIONS` and `ALLIANCE_INSTRUCTIONS_APPENDIX` — reduces per-session token cost; no behavior change. Replaced colliding Alliance appendix steps 4–5 with unnumbered "Manual Alliance flow" bullets (includes `PINECONE_DISABLE_SUGGEST_FLOW=true` escape clause). Full detail remains in [docs/CONFIGURATION.md](docs/CONFIGURATION.md). ## [0.4.0] - 2026-06-24 diff --git a/README.md b/README.md index bfd9ee3..85c8138 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: BSL-1.0](https://img.shields.io/badge/License-BSL--1.0-blue.svg)](https://opensource.org/licenses/BSL-1.0) [![CI](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/workflows/CI/badge.svg)](https://github.com/cppalliance/pinecone-read-only-mcp-typescript/actions) -A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that implements the MCP specification via `@modelcontextprotocol/sdk` v1.25+ and provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking. +A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that implements the MCP specification via `@modelcontextprotocol/sdk` v1.29+ and provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking. Current version: 0.4.0 (npm `latest` after publish). Pin `@0.4.0` in install and MCP config for reproducible upgrades. diff --git a/docs/PACKAGE_SPLIT_EVAL.md b/docs/PACKAGE_SPLIT_EVAL.md index 600e59b..2d103e1 100644 --- a/docs/PACKAGE_SPLIT_EVAL.md +++ b/docs/PACKAGE_SPLIT_EVAL.md @@ -182,7 +182,7 @@ Both packages would share the same four production dependencies from the root `p | Dependency | Current range | Alignment note | | ---------- | ------------- | -------------- | -| `@modelcontextprotocol/sdk` | `^1.25.3` | MCP protocol surface; must stay aligned across core and Alliance | +| `@modelcontextprotocol/sdk` | `^1.29.0` | MCP protocol surface; must stay aligned across core and Alliance | | `@pinecone-database/pinecone` | `^7.1.0` | Client API used by `PineconeClient`; core owns the wrapper | | `dotenv` | `^17.2.3` | Env loading in config resolvers | | `zod` | `^4.3.6` | Tool input schemas and response validation | diff --git a/package-lock.json b/package-lock.json index 0b55e97..ec01ea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.4.0", "license": "BSL-1.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.29.0", "@pinecone-database/pinecone": "^8.0.0", "dotenv": "^17.2.3", "zod": "^4.3.6" diff --git a/package.json b/package.json index bb7e72b..09dfb75 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "prepack": "npm run build" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.29.0", "@pinecone-database/pinecone": "^8.0.0", "dotenv": "^17.2.3", "zod": "^4.3.6" diff --git a/src/__tests__/mcp-rc-readiness.test.ts b/src/__tests__/mcp-rc-readiness.test.ts new file mode 100644 index 0000000..d08fe89 --- /dev/null +++ b/src/__tests__/mcp-rc-readiness.test.ts @@ -0,0 +1,171 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { LATEST_PROTOCOL_VERSION, type JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; +import { SERVER_NAME, SERVER_VERSION } from '../constants.js'; +import { setupCoreServer, teardownServer, type ServerHandle } from '../core/setup.js'; +import { setupAllianceServer } from '../alliance/setup.js'; +import { resolveAllianceConfig } from '../alliance/config.js'; +import { createIsolatedContext } from '../core/server/server-context.js'; +import { + createTestServerContext, + isolateFromDefaultContext, + makeMockPineconeClient, +} from '../core/server/tools/test-helpers.js'; + +/** + * End-to-end MCP verification harness (#202). + * + * Drives the real `McpServer` over an in-memory transport with the SDK's own + * `Client`, so a future SDK bump (the 2026-07-28 RC protocol revision, once it + * ships) is verified in one place: the initialize handshake and protocol + * negotiation, the full registered tool surface, and a round-trip tool call. + * The protocol assertions key off the SDK's `LATEST_PROTOCOL_VERSION`, so when + * the RC SDK is pinned they re-check the server against the new revision with no + * edits here. If the RC is not published in time, this still guards the current + * pinned SDK. + */ + +const CORE_TOOLS = [ + 'list_namespaces', + 'namespace_router', + 'count', + 'query', + 'keyword_search', + 'query_documents', + 'generate_urls', + 'guided_query', +].sort(); + +/** A fresh, isolated core server (own context + mock client) so several can coexist. */ +async function freshCoreServer(namespaces: string[] = ['ns']): Promise { + const ctx = createTestServerContext({ client: makeMockPineconeClient(namespaces) as never }); + return setupCoreServer({ context: ctx }); +} + +/** Link the SDK Client to a live server over paired in-memory transports. */ +async function connectClient(server: ServerHandle): Promise { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + const client = new Client({ name: 'rc-readiness-harness', version: '0.0.0' }); + await client.connect(clientTransport); + return client; +} + +/** Send a raw JSON-RPC initialize and return the negotiated result. */ +async function rawInitialize( + server: ServerHandle, + requestedProtocolVersion: string +): Promise<{ protocolVersion: string; serverInfo: { name: string; version: string } }> { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + try { + return await new Promise<{ + protocolVersion: string; + serverInfo: { name: string; version: string }; + }>((resolve, reject) => { + clientTransport.onmessage = (message: JSONRPCMessage) => { + if (!('id' in message) || message.id !== 1) return; + if ('error' in message) { + reject(new Error(message.error.message)); + } else if ('result' in message) { + resolve( + message.result as { + protocolVersion: string; + serverInfo: { name: string; version: string }; + } + ); + } + }; + void clientTransport + .start() + .then(() => + clientTransport.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: requestedProtocolVersion, + capabilities: {}, + clientInfo: { name: 'raw-harness', version: '0.0.0' }, + }, + }) + ) + .catch(reject); + }); + } finally { + await clientTransport.close(); + } +} + +describe('MCP RC-readiness harness (#202)', () => { + afterEach(() => { + teardownServer(); + isolateFromDefaultContext(); + }); + + it('initializes over the transport and round-trips server metadata', async () => { + const server = await freshCoreServer(); + const client = await connectClient(server); + + // A resolved connect proves the initialize handshake and protocol + // negotiation succeeded: the SDK Client throws if the server answers with a + // protocolVersion outside SUPPORTED_PROTOCOL_VERSIONS. + expect(client.getServerVersion()).toEqual({ name: SERVER_NAME, version: SERVER_VERSION }); + expect(client.getServerCapabilities()?.tools).toBeDefined(); + expect(client.getInstructions()).toBeTruthy(); + + await client.close(); + }); + + it('exposes the full core tool surface through tools/list', async () => { + const server = await freshCoreServer(); + const client = await connectClient(server); + + const names = (await client.listTools()).tools.map((t) => t.name).sort(); + expect(names).toEqual(CORE_TOOLS); + + await client.close(); + }); + + it('registers the Alliance suggest_query_params tool on top of the core surface', async () => { + const ctx = createIsolatedContext( + resolveAllianceConfig({ apiKey: 'sk-test', indexName: 'test-index' }), + { client: makeMockPineconeClient(['ns']) as never } + ); + const server = await setupAllianceServer({ context: ctx }); + const client = await connectClient(server); + + const names = (await client.listTools()).tools.map((t) => t.name); + for (const core of CORE_TOOLS) expect(names).toContain(core); + expect(names).toContain('suggest_query_params'); + + await client.close(); + }); + + it('round-trips a tool call over the transport', async () => { + const server = await freshCoreServer(['alpha', 'beta']); + const client = await connectClient(server); + + const res = await client.callTool({ name: 'list_namespaces', arguments: {} }); + expect(res.isError ?? false).toBe(false); + // The seeded namespaces must round-trip through the handler and transport, + // not just any array, so a regression returning empty/garbage content fails. + const text = (res.content as Array<{ type: string; text: string }>)[0]?.text ?? ''; + expect(text).toContain('alpha'); + expect(text).toContain('beta'); + + await client.close(); + }); + + it('negotiates the SDK latest protocol version and falls back for an unknown request', async () => { + // A server connects to one transport for its lifetime, so use a fresh one per probe. + const negotiated = await rawInitialize(await freshCoreServer(), LATEST_PROTOCOL_VERSION); + expect(negotiated.protocolVersion).toBe(LATEST_PROTOCOL_VERSION); + expect(negotiated.serverInfo).toMatchObject({ name: SERVER_NAME, version: SERVER_VERSION }); + + // An unsupported request must not error; the server falls back to its latest. + const fallback = await rawInitialize(await freshCoreServer(), 'not-a-real-protocol-version'); + expect(fallback.protocolVersion).toBe(LATEST_PROTOCOL_VERSION); + }); +});