Skip to content

Commit 91ff308

Browse files
jonathanMLDevzhocursoragent
authored
Add per-source descriptions and per-namespace declared schemas from p… (#204)
* Add per-source descriptions and per-namespace declared schemas from private config * fixed format errors * Address PR review: fix test-search script, tighten description validation, harden tests/docs Co-authored-by: Cursor <cursoragent@cursor.com> * Fix stale-namespace warnings and surface namespace descriptions on list_namespaces * addressed reviews: not.toContain(corpusPlaceholder) - clearest intent: "this exact text must not be in the instructions." * created namespace-cache.ts for removing the duplication --------- Co-authored-by: zho <jornathanm910923@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d087a5e commit 91ff308

36 files changed

Lines changed: 1150 additions & 180 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- **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).
14+
- **`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.
15+
1116
### Changed
1217

18+
- **Breaking (`list_sources`):** response shape `sources: string[]``sources: { name, description? }[]`. Migration: [MIGRATION.md § Unreleased list_sources](docs/MIGRATION.md#unreleased-list_sources-response-shape).
19+
- **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).
1320
- **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).
1421

1522
## [0.4.0] - 2026-06-24

benchmarks/latency.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,10 @@ function createBenchPineconeMock(): PineconeClient {
233233
return { count: 42, truncated: false };
234234
},
235235
async listNamespacesWithMetadata() {
236-
return namespaces;
236+
return {
237+
namespaces: namespaces.map((n) => ({ ...n, schema_source: 'sampled' as const })),
238+
warnings: [],
239+
};
237240
},
238241
async listNamespacesFromKeywordIndex() {
239242
return namespaces.map((n) => ({ namespace: n.namespace, recordCount: n.recordCount }));

docs/CONFIGURATION.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ Set `PINECONE_CONFIG_FILE` (or `--config-file`) to a path such as [examples/mult
7575

7676
Values support `${ENV_VAR}` indirection (resolved at startup). Per-source `sparseIndexName` and `rerankModel` are optional; Alliance defaults apply when omitted.
7777

78+
Optional **config-file-only** fields (not supported in inline `PINECONE_SOURCES`):
79+
80+
| Field | Scope | Purpose |
81+
| ----- | ----- | ------- |
82+
| `description` | Per source | Short corpus/content hint surfaced via `list_sources` (helps LLM routing across sources or MCP entries) |
83+
| `namespaces` | Per source | Map of namespace name → `{ description?, metadata_schema? }`; per-namespace `description` is surfaced via `list_namespaces` on matching live rows |
84+
85+
`metadata_schema` is a flat `fieldName → type` map (same vocabulary as `list_namespaces``metadata_fields`, e.g. `"title": "string"`). When declared for a live namespace, the server **skips live sampling** for that namespace and trusts the declared schema until the config file changes. Namespaces declared in config but absent from Pinecone produce a non-fatal `config_warnings` entry in `list_namespaces` (never a startup failure).
86+
87+
**Never** commit real corpus descriptions, namespace names, or internal field names to the open-source repo — use generic placeholders in examples only. Real values belong in staff-machine private config files per [Deployment profiles](#deployment-profiles). See [SECURITY.md](./SECURITY.md).
88+
7889
### MCP tools and routing
7990

8091
| Tool | `source` parameter |
@@ -131,7 +142,7 @@ Multi-source mode supports two operational profiles. **Never** ship a merged int
131142
}
132143
```
133144

134-
Prefer `PINECONE_CONFIG_FILE` with `${ENV_VAR}` indirection over inline API keys in `PINECONE_SOURCES`. See [SECURITY.md](./SECURITY.md).
145+
Prefer `PINECONE_CONFIG_FILE` with `${ENV_VAR}` indirection over inline API keys in `PINECONE_SOURCES`. For internal deployments, add optional `description` and `namespaces` declarations in the JSON config file on staff machines only — never in public examples or committed constants. See [SECURITY.md](./SECURITY.md).
135146

136147
### Architecture decision
137148

docs/MIGRATION.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,56 @@ This guide is for **library and MCP client authors** upgrading from earlier **0.
66

77
Under [semver 0.y.z](https://semver.org/spec/v2.0.0.html#spec-item-4), **0.1.x → 0.2.0 is a breaking minor** — pin `@0.2.0` only after reading this guide.
88

9+
## Unreleased: `list_sources` response shape
10+
11+
**Who is affected:** MCP clients parsing `list_sources` success JSON in multi-source mode.
12+
13+
**Before:**
14+
15+
```json
16+
{
17+
"status": "success",
18+
"sources": ["api_key_1", "api_key_2"],
19+
"default": "api_key_1"
20+
}
21+
```
22+
23+
**After:**
24+
25+
```json
26+
{
27+
"status": "success",
28+
"sources": [
29+
{ "name": "api_key_1", "description": "Optional corpus hint from private config" },
30+
{ "name": "api_key_2" }
31+
],
32+
"default": "api_key_1"
33+
}
34+
```
35+
36+
Read `sources[].name` instead of treating `sources` as `string[]`. `description` is omitted when not configured.
37+
38+
## Unreleased: `PineconeClient.listNamespacesWithMetadata` return shape
39+
40+
**Who is affected:** Direct library consumers of `PineconeClient` (not MCP tool clients — `list_namespaces` tool response shape is additive only).
41+
42+
**Before:**
43+
44+
```ts
45+
const rows = await client.listNamespacesWithMetadata();
46+
// rows: Array<{ namespace, recordCount, metadata }>
47+
```
48+
49+
**After:**
50+
51+
```ts
52+
const { namespaces, warnings } = await client.listNamespacesWithMetadata(declaredSchemas);
53+
// namespaces: Array<{ namespace, recordCount, metadata, schema_source }>
54+
// warnings: string[] — e.g. stale declared namespace not found live
55+
```
56+
57+
Optional `declaredSchemas` argument skips live sampling for namespaces with a declared schema.
58+
959
## Unreleased: Stable vs experimental response fields
1060

1161
**Rationale:** Tool success payloads mixed stable contract fields with experimental diagnostics (`degraded`, `decision_trace`, etc.) at the top level. Experimental fields are now nested under `experimental` so consumers know which fields are safe across minor version bumps.

docs/SECURITY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Tool responses returned to MCP clients (and LLM consumers) are sanitized in `src
2323

2424
This covers tool error payloads, hybrid degradation reasons, and SDK error text surfaced in DEBUG log mode — not only stderr logs.
2525

26+
## Private config content (descriptions and schemas)
27+
28+
Per-source `description` and per-namespace `namespaces` (including namespace names and `metadata_schema` field names) loaded from `PINECONE_CONFIG_FILE` are **High**-risk if committed to the open-source repo or shared through public distribution channels — they can disclose what private corpora contain and which internal metadata fields exist. Keep real values on staff machines only; use generic placeholders in examples and tests. Same deployment-profile separation as API keys — see [CONFIGURATION.md § Deployment profiles](./CONFIGURATION.md#deployment-profiles).
29+
2630
## Docker image
2731

2832
The multi-stage [`Dockerfile`](../Dockerfile):

docs/TOOLS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Success payloads separate **stable** fields (safe across minor bumps after `1.0.
88

99
| Tool | Stable | Experimental |
1010
| ---- | ------ | ------------ |
11-
| `list_sources` | `status`, `sources`, `default` | _(none)_ |
12-
| `list_namespaces` | `status`, `cache_hit`, `cache_ttl_seconds`, `expires_at_iso`, `count`, `namespaces`, optional `source_errors` | _(none)_ |
11+
| `list_sources` | `status`, `sources` (`{ name, description? }[]`), `default` | _(none)_ |
12+
| `list_namespaces` | `status`, `cache_hit`, `cache_ttl_seconds`, `expires_at_iso`, `count`, `namespaces`, optional `source_errors`, optional `config_warnings`, optional per-row `schema_source` | _(none)_ |
1313
| `namespace_router` | `status`, `cache_hit`, `user_query`, `suggestions`, `recommended_namespace`, optional `recommended_source` | _(none)_ |
1414
| `suggest_query_params` | `status`, `cache_hit`, `suggested_fields`, `recommended_tool`, `use_count_tool`, `explanation`, `namespace_found`, optional `source` | _(none)_ |
1515
| `count` | `status`, `count`, `truncated`, `namespace`, `metadata_filter`, optional `source` | _(none)_ |
@@ -73,7 +73,7 @@ Registered only when more than one Pinecone source is configured.
7373
| | |
7474
| --- | --- |
7575
| **Input** | _(empty object)_ |
76-
| **Success** | `{ status: 'success', sources: string[], default: string }` |
76+
| **Success** | `{ status: 'success', sources: { name, description? }[], default: string }` |
7777
| **Errors** | `LIFECYCLE` when not in multi-source mode |
7878

7979
**Example:**
@@ -91,7 +91,7 @@ Registered only when more than one Pinecone source is configured.
9191
| | |
9292
| --- | --- |
9393
| **Input** | Optional `source` — filter to one configured project |
94-
| **Success** | `{ status: 'success', cache_hit, cache_ttl_seconds, expires_at_iso, count, namespaces: [{ name, record_count, metadata_fields, source? }], source_errors? }` |
94+
| **Success** | `{ status: 'success', cache_hit, cache_ttl_seconds, expires_at_iso, count, namespaces: [{ name, record_count, metadata_fields, source?, schema_source?, description? }], source_errors?, config_warnings? }` |
9595
| **Errors** | `PINECONE_ERROR`, `TIMEOUT`, etc. |
9696

9797
**Example (multi-source, all projects):**

examples/alliance/demo-mock-pinecone-client.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type HybridQueryResult,
1111
type KeywordIndexNamespacesResult,
1212
type KeywordSearchParams,
13+
type NamespacesWithMetadataResult,
1314
type PineconeMetadataValue,
1415
type QueryParams,
1516
type SearchResult,
@@ -36,21 +37,26 @@ export class DemoMockPineconeClient extends PineconeClient {
3637
super({ apiKey: '00000000-0000-0000-0000-000000000000', indexName: 'demo-index' });
3738
}
3839

39-
override async listNamespacesWithMetadata(): Promise<
40-
Array<{ namespace: string; recordCount: number; metadata: Record<string, string> }>
41-
> {
42-
return [
43-
{
44-
namespace: DEMO_NAMESPACE,
45-
recordCount: 42,
46-
metadata: {
47-
document_number: 'string',
48-
title: 'string',
49-
chunk_text: 'string',
50-
url: 'string',
40+
override async listNamespacesWithMetadata(
41+
_declaredSchemas?: Record<string, Record<string, string>>,
42+
_declaredNamespaceNames?: string[]
43+
): Promise<NamespacesWithMetadataResult> {
44+
return {
45+
namespaces: [
46+
{
47+
namespace: DEMO_NAMESPACE,
48+
recordCount: 42,
49+
metadata: {
50+
document_number: 'string',
51+
title: 'string',
52+
chunk_text: 'string',
53+
url: 'string',
54+
},
55+
schema_source: 'sampled',
5156
},
52-
},
53-
];
57+
],
58+
warnings: [],
59+
};
5460
}
5561

5662
override async listNamespacesFromKeywordIndex(): Promise<KeywordIndexNamespacesResult> {

examples/multi-source/pinecone-sources.json.example

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
"sources": {
44
"api_key_1": {
55
"apiKey": "${PINECONE_API_KEY_1}",
6-
"indexName": "index_name_1"
6+
"indexName": "index_name_1",
7+
"description": "<optional: describe this corpus in your PRIVATE staff config, not here>",
8+
"namespaces": {
9+
"example-namespace": {
10+
"description": "<optional: describe this namespace in your PRIVATE staff config, not here>",
11+
"metadata_schema": {
12+
"field_a": "string",
13+
"field_b": "number"
14+
}
15+
}
16+
}
717
},
818
"api_key_2": {
919
"apiKey": "${PINECONE_API_KEY_2}",

scripts/test-search.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,27 @@ async function test() {
3636
// Test 1: List namespaces with metadata
3737
console.log('\n📋 Test 1: Listing namespaces with metadata...');
3838
const namespacesInfo = await client.listNamespacesWithMetadata();
39-
console.log(`✅ Found ${namespacesInfo.length} namespace(s):`);
40-
namespacesInfo.forEach((ns) => {
39+
console.log(`✅ Found ${namespacesInfo.namespaces.length} namespace(s):`);
40+
namespacesInfo.namespaces.forEach((ns) => {
4141
console.log(` - ${ns.namespace}: ${ns.recordCount} records`);
4242
if (Object.keys(ns.metadata).length > 0) {
4343
console.log(` Metadata fields:`, Object.keys(ns.metadata));
4444
}
4545
});
4646

47-
if (namespacesInfo.length === 0) {
47+
if (namespacesInfo.warnings.length > 0) {
48+
console.log('⚠️ Config warnings:');
49+
namespacesInfo.warnings.forEach((warning) => console.log(` - ${warning}`));
50+
}
51+
52+
if (namespacesInfo.namespaces.length === 0) {
4853
console.log('⚠️ No namespaces found in your index');
4954
console.log(' Make sure your Pinecone index has data');
5055
return;
5156
}
5257

5358
// Test 2: Query without reranking (faster)
54-
const testNamespace = namespacesInfo[0].namespace;
59+
const testNamespace = namespacesInfo.namespaces[0].namespace;
5560
console.log(`\n🔍 Test 2: Query WITHOUT reranking (faster)`);
5661
console.log(` Namespace: "${testNamespace}"`);
5762
console.log(` Query: "test query"`);
@@ -95,7 +100,7 @@ async function test() {
95100
}
96101

97102
// Test 4: Query with metadata filter on wg21-papers namespace
98-
const wg21Namespace = namespacesInfo.find((ns) => ns.namespace === 'wg21-papers');
103+
const wg21Namespace = namespacesInfo.namespaces.find((ns) => ns.namespace === 'wg21-papers');
99104
let duration3: number | undefined;
100105

101106
if (wg21Namespace) {
@@ -149,7 +154,7 @@ async function test() {
149154
} else {
150155
console.log(`\n⚠️ Test 4 skipped: "wg21-papers" namespace not found`);
151156
console.log(
152-
` Available namespaces: ${namespacesInfo.map((ns) => ns.namespace).join(', ')}`
157+
` Available namespaces: ${namespacesInfo.namespaces.map((ns) => ns.namespace).join(', ')}`
153158
);
154159
}
155160

src/alliance/tools/isolated-context.context.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isolateFromDefaultContext,
88
makeHybridQueryResult,
99
makeSearchResult,
10+
mockNamespacesWithMetadataResult,
1011
parseToolJson,
1112
} from '../../core/server/tools/test-helpers.js';
1213
import {
@@ -27,18 +28,20 @@ describe('isolated ServerContext with zero default context', () => {
2728
});
2829

2930
it('guided_query enrich_urls uses ctx builtins, not default registry', async () => {
30-
const listNamespacesWithMetadata = vi.fn().mockResolvedValue([
31-
{
32-
namespace: 'mailing',
33-
recordCount: 42,
34-
metadata: {
35-
document_number: 'string',
36-
title: 'string',
37-
author: 'string',
38-
chunk_text: 'string',
31+
const listNamespacesWithMetadata = vi.fn().mockResolvedValue(
32+
mockNamespacesWithMetadataResult([
33+
{
34+
namespace: 'mailing',
35+
recordCount: 42,
36+
metadata: {
37+
document_number: 'string',
38+
title: 'string',
39+
author: 'string',
40+
chunk_text: 'string',
41+
},
3942
},
40-
},
41-
]);
43+
])
44+
);
4245
const query = vi.fn().mockResolvedValue(
4346
makeHybridQueryResult({
4447
results: [

0 commit comments

Comments
 (0)