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
60 changes: 37 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (MCP) server that 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.25+ and provides semantic search over Pinecone vector databases using hybrid search (dense + sparse) with reranking.

**Current version: 0.2.0** (npm `latest` after publish). Pin `@0.2.0` in install and MCP config for reproducible upgrades.

Expand All @@ -19,18 +19,18 @@ While the package is **`0.y.z`**, minor releases may include breaking changes ([

## Documentation

| Doc | Description |
| ---------------------------------------------- | -------------------------------------- |
| [docs/README.md](docs/README.md) | Index of all guides |
| [docs/TOOLS.md](docs/TOOLS.md) | Tool catalog & flows |
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Env vars, CLI flags, library config |
| [docs/FAQ.md](docs/FAQ.md) | Common questions |
| [docs/MIGRATION.md](docs/MIGRATION.md) | Deprecations & breaking changes |
| [docs/deprecation-policy.md](docs/deprecation-policy.md) | Release & deprecation policy |
| [docs/CI_CD.md](docs/CI_CD.md) | GitHub Actions, SBOM, Docker, releases |
| [docs/RELEASING.md](docs/RELEASING.md) | npm publish via GitHub Releases |
| [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | How to contribute |
| [docs/SECURITY.md](docs/SECURITY.md) | Vulnerability reporting |
| Doc | Description |
| -------------------------------------------------------- | -------------------------------------- |
| [docs/README.md](docs/README.md) | Index of all guides |
| [docs/TOOLS.md](docs/TOOLS.md) | Tool catalog & flows |
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Env vars, CLI flags, library config |
| [docs/FAQ.md](docs/FAQ.md) | Common questions |
| [docs/MIGRATION.md](docs/MIGRATION.md) | Deprecations & breaking changes |
| [docs/deprecation-policy.md](docs/deprecation-policy.md) | Release & deprecation policy |
| [docs/CI_CD.md](docs/CI_CD.md) | GitHub Actions, SBOM, Docker, releases |
| [docs/RELEASING.md](docs/RELEASING.md) | npm publish via GitHub Releases |
| [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | How to contribute |
| [docs/SECURITY.md](docs/SECURITY.md) | Vulnerability reporting |

## Error responses

Expand Down Expand Up @@ -149,7 +149,11 @@ Module-level singleton facades (`setPineconeClient`, `registerUrlGenerator`, `ge
```ts
const config = resolveAllianceConfig({ apiKey: '...' });
const ctx = createServer(config);
ctx.setClient(new PineconeClient({ /* ... */ }));
ctx.setClient(
new PineconeClient({
/* ... */
})
);
const server = await setupAllianceServer({ context: ctx });
```

Expand All @@ -163,9 +167,16 @@ Process-default / facade-based setup remains available during the deprecation wi

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

setPineconeClient(new PineconeClient({ /* ... */ }));
setPineconeClient(
new PineconeClient({
/* ... */
})
);
const server = await setupAllianceServer(resolveAllianceConfig({ apiKey: '...' }));
// Call teardownServer() before re-initializing the process-default context.
```
Expand All @@ -185,7 +196,10 @@ import {
type UrlGenerationResult,
type UrlGeneratorFn,
} from '@will-cppa/pinecone-read-only-mcp';
import { resolveAllianceConfig, setupAllianceServer } from '@will-cppa/pinecone-read-only-mcp/alliance';
import {
resolveAllianceConfig,
setupAllianceServer,
} from '@will-cppa/pinecone-read-only-mcp/alliance';

const config = resolveAllianceConfig({ apiKey: '...' }); // optional: indexName, rerankModel
const ctx = createServer(config);
Expand Down Expand Up @@ -217,12 +231,12 @@ A fuller embedding sample lives in [examples/alliance/custom-url-generator.ts](e

**Alliance / advanced** — [examples/alliance/](examples/alliance/):

| File | Description |
| ---- | ----------- |
| [examples/alliance/suggest-flow-demo.ts](examples/alliance/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
| [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) | **setupAllianceServer** without the CLI |
| [examples/alliance/custom-url-generator.ts](examples/alliance/custom-url-generator.ts) | Custom **URL generator** registration |
| File | Description |
| ------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| [examples/alliance/suggest-flow-demo.ts](examples/alliance/suggest-flow-demo.ts) | Manual **suggest_query_params → query** flow |
| [examples/alliance/guided-query-demo.ts](examples/alliance/guided-query-demo.ts) | **guided_query** and `experimental.decision_trace` |
| [examples/alliance/library-embedding-demo.ts](examples/alliance/library-embedding-demo.ts) | **setupAllianceServer** without the CLI |
| [examples/alliance/custom-url-generator.ts](examples/alliance/custom-url-generator.ts) | Custom **URL generator** registration |

Run with `npx tsx examples/<path>.ts` from a checkout (requires valid Pinecone env for live paths). See [examples/README.md](examples/README.md).

Expand Down
43 changes: 43 additions & 0 deletions src/core/server/response-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,49 @@ describe('response-schemas', () => {
expect(() => keywordSearchSuccessResponseSchema.parse({ status: 'success' })).toThrow();
});

it('accepts every strict query payload in the permissive derived schema', () => {
const strictPayload = {
status: 'success' as const,
mode: 'query_fast' as const,
query: 'q',
namespace: 'wg21',
result_count: 1,
results: [
{
document_id: 'D1',
paper_number: 'D1',
title: 'T',
author: 'A',
url: '',
content: 'c',
score: 0.9,
reranked: false,
},
],
};
expect(querySuccessResponseSchema.parse(strictPayload)).toBeDefined();
expect(queryResponseSchema.parse(strictPayload)).toBeDefined();
});

it('accepts permissive-only keyword search payload but rejects strict validation', () => {
const permissiveOnly = { status: 'success' as const };
expect(keywordSearchResponseSchema.parse(permissiveOnly)).toBeDefined();
expect(() => keywordSearchSuccessResponseSchema.parse(permissiveOnly)).toThrow();
});

it('accepts every strict keyword search payload in the permissive derived schema', () => {
const strictPayload = {
status: 'success' as const,
query: 'kw',
namespace: 'wg21',
index: 'test-index-sparse',
result_count: 0,
results: [] as [],
};
expect(keywordSearchSuccessResponseSchema.parse(strictPayload)).toBeDefined();
expect(keywordSearchResponseSchema.parse(strictPayload)).toBeDefined();
});

it('rejects query response missing status', () => {
expect(() => queryResponseSchema.parse({ results: [] })).toThrow();
});
Expand Down
59 changes: 29 additions & 30 deletions src/core/server/response-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* Zod schemas for MCP tool success responses.
* Types are derived via `z.infer` — single source of truth for response contracts.
*
* Single canonical schema per response type. Handler-boundary schemas define required
* stable fields. Permissive exports are derived via `.partial()` for `guided_query` union
* reuse and backward-compatible client validation.
*/

import { z } from 'zod';
Expand Down Expand Up @@ -70,21 +74,7 @@ export const guidedQueryExperimentalSchema = z.object({
decision_trace: guidedQueryDecisionTraceSchema,
});

export const queryResponseSchema = z.object({
status: z.literal('success'),
mode: z.enum(['query', 'query_fast', 'query_detailed']).optional(),
query: z.string().optional(),
namespace: z.string().optional(),
metadata_filter: z.record(z.string(), z.unknown()).optional(),
result_count: z.number().optional(),
fields: z.array(z.string()).optional(),
results: z.array(queryResultRowSchema).optional(),
experimental: queryExperimentalSchema.optional(),
});

export type QueryResponse = z.infer<typeof queryResponseSchema>;

/** Strict handler-boundary schema for `query` / `query_fast` / `query_detailed` success payloads. */
/** Handler-boundary schema for `query` / `query_fast` / `query_detailed` success payloads. */
export const querySuccessResponseSchema = z.object({
status: z.literal('success'),
mode: z.enum(['query', 'query_fast', 'query_detailed']),
Expand All @@ -99,6 +89,17 @@ export const querySuccessResponseSchema = z.object({

export type QuerySuccessResponse = z.infer<typeof querySuccessResponseSchema>;

/** Permissive query shape for `guided_query` union reuse and client-side validation. */
export const queryResponseSchema = querySuccessResponseSchema.partial({
mode: true,
query: true,
namespace: true,
result_count: true,
results: true,
});

export type QueryResponse = z.infer<typeof queryResponseSchema>;

export const listNamespacesResponseSchema = z.object({
status: z.literal('success'),
cache_hit: z.boolean(),
Expand Down Expand Up @@ -148,21 +149,7 @@ export const countResponseSchema = z.object({

export type CountResponse = z.infer<typeof countResponseSchema>;

export const keywordSearchResponseSchema = z.object({
status: z.literal('success'),
query: z.string().optional(),
namespace: z.string().optional(),
index: z.string().optional(),
metadata_filter: z.record(z.string(), z.unknown()).optional(),
result_count: z.number().optional(),
results: z.array(queryResultRowSchema).optional(),
fields: z.array(z.string()).optional(),
});

/** @deprecated Import from `response-schemas` / package root; alias kept for one minor cycle. */
export type KeywordSearchResponse = z.infer<typeof keywordSearchResponseSchema>;

/** Strict handler-boundary schema for `keyword_search` success payloads. */
/** Handler-boundary schema for `keyword_search` success payloads. */
export const keywordSearchSuccessResponseSchema = z.object({
status: z.literal('success'),
query: z.string(),
Expand All @@ -176,6 +163,18 @@ export const keywordSearchSuccessResponseSchema = z.object({

export type KeywordSearchSuccessResponse = z.infer<typeof keywordSearchSuccessResponseSchema>;

/** Permissive keyword_search shape for client-side validation. */
export const keywordSearchResponseSchema = keywordSearchSuccessResponseSchema.partial({
query: true,
namespace: true,
index: true,
result_count: true,
results: true,
});

/** @deprecated Import from `response-schemas` / package root; alias kept for one minor cycle. */
export type KeywordSearchResponse = z.infer<typeof keywordSearchResponseSchema>;

const queryDocumentRowSchema = z.object({
document_id: z.string(),
merged_content: z.string(),
Expand Down
4 changes: 2 additions & 2 deletions src/core/server/tools/keyword-search-tool.context.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { registerKeywordSearchTool } from './keyword-search-tool.js';
import { keywordSearchResponseSchema } from '../response-schemas.js';
import { keywordSearchSuccessResponseSchema } from '../response-schemas.js';
import {
createMockServer,
createTestServerContext,
Expand All @@ -27,7 +27,7 @@ describe('keyword_search tool handler (ServerContext instance path)', () => {
top_k: 5,
});
const body = parseToolJson(raw);
expectMatchesResponseSchema(keywordSearchResponseSchema, body);
expectMatchesResponseSchema(keywordSearchSuccessResponseSchema, body);
expect(body).toMatchObject({
status: 'success',
query: 'contracts',
Expand Down
5 changes: 3 additions & 2 deletions src/core/server/tools/keyword-search-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {
import {
keywordSearchSuccessResponseSchema,
type KeywordSearchResponse,
type KeywordSearchSuccessResponse,
} from '../response-schemas.js';
import { jsonErrorResponse, validatedJsonResponse } from '../tool-response.js';

/** @deprecated Import {@link KeywordSearchResponse} from `response-schemas` or package root. */
export type { KeywordSearchResponse };

type KeywordSearchExecResult =
| { ok: true; body: KeywordSearchResponse }
| { ok: true; body: KeywordSearchSuccessResponse }
| { ok: false; error: ToolError };

async function executeKeywordSearch(
Expand Down Expand Up @@ -81,7 +82,7 @@ async function executeKeywordSearch(
ctx,
});

const response: KeywordSearchResponse = {
const response: KeywordSearchSuccessResponse = {
status: 'success',
query: normalizedQuery,
namespace: normalizedNamespace,
Expand Down
4 changes: 2 additions & 2 deletions src/core/server/tools/query-tool.context.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { registerQueryTool } from './query-tool.js';
import { queryResponseSchema } from '../response-schemas.js';
import { querySuccessResponseSchema } from '../response-schemas.js';
import {
assertToolErrorCode,
createMockServer,
Expand Down Expand Up @@ -30,7 +30,7 @@ describe('query tool handler (ServerContext instance path)', () => {
preset: 'fast',
});
const body = parseToolJson(raw);
expectMatchesResponseSchema(queryResponseSchema, body);
expectMatchesResponseSchema(querySuccessResponseSchema, body);
expect(body).toMatchObject({
status: 'success',
mode: 'query_fast',
Expand Down
9 changes: 6 additions & 3 deletions src/core/server/tools/query-tool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../../constants.js';
import type { QueryResponse } from '../../../types.js';
import { getPineconeClient } from '../client-context.js';
import { formatQueryResultRows } from '../format-query-result.js';
import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js';
Expand All @@ -15,7 +14,11 @@ import {
logToolError,
validationToolError,
} from '../tool-error.js';
import { buildQueryExperimental, querySuccessResponseSchema } from '../response-schemas.js';
import {
buildQueryExperimental,
querySuccessResponseSchema,
type QuerySuccessResponse,
} from '../response-schemas.js';
import { jsonErrorResponse, validatedJsonResponse } from '../tool-response.js';

type QueryMode = 'query' | 'query_fast' | 'query_detailed';
Expand Down Expand Up @@ -76,7 +79,7 @@ async function executeQuery(params: QueryExecParams, ctx?: ServerContext) {

const formattedResults = formatQueryResultRows(queryOutcome.results, { ctx });

const response: QueryResponse = {
const response: QuerySuccessResponse = {
status: 'success',
mode,
query: query_text,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export type KeywordIndexNamespacesResult =
export type {
QueryResultRowShape,
QueryResponse,
QuerySuccessResponse,
KeywordSearchResponse,
KeywordSearchSuccessResponse,
} from './core/server/response-schemas.js';

/** Internal merged hit shape before rerank (dense + sparse deduped). */
Expand Down
Loading