From 434d791550301dff232be304008ccf2778d80fea Mon Sep 17 00:00:00 2001 From: zho Date: Wed, 17 Jun 2026 01:26:35 +0800 Subject: [PATCH 1/2] feat: expose guided_query in core and document core/Alliance gate defaults --- CHANGELOG.md | 3 +- docs/CONFIGURATION.md | 2 + docs/MIGRATION.md | 11 + docs/TOOLS.md | 4 +- src/__tests__/cross-entry-point.test.ts | 93 ++++++ src/alliance/config.ts | 7 + src/alliance/setup.ts | 4 +- src/alliance/tools/guided-query-tool.ts | 273 +----------------- src/constants.test.ts | 4 +- src/constants.ts | 11 +- src/core/config.ts | 13 +- src/core/server/redaction.test.ts | 2 +- .../tools/guided-query-tool.context.test.ts | 10 +- .../server}/tools/guided-query-tool.test.ts | 12 +- src/core/server/tools/guided-query-tool.ts | 269 +++++++++++++++++ src/core/setup.ts | 11 +- 16 files changed, 427 insertions(+), 302 deletions(-) create mode 100644 src/__tests__/cross-entry-point.test.ts rename src/{alliance => core/server}/tools/guided-query-tool.context.test.ts (95%) rename src/{alliance => core/server}/tools/guided-query-tool.test.ts (94%) create mode 100644 src/core/server/tools/guided-query-tool.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f86e417..0e7a90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release ### Added +- `guided_query` tool registered by `setupCoreServer` / package-root import (core-layer handler; no Alliance URL generators or index defaults required). - `ServerContextComposition` interface plus `NamespaceCacheSeed` and `SuggestionFlowSeedEntry` types for dependency injection into `ServerContext`. - `createIsolatedContext(config, composition?)` factory for multi-tenant embedders (no process-global side effects). - Zod schemas for all nine MCP tool success responses (`queryResponseSchema`, `guidedQueryResponseSchema`, etc.) exported from the package root for client-side validation. Success payloads are runtime-validated before return. @@ -22,7 +23,7 @@ Tagged releases are published to npm from GitHub Actions when a **GitHub Release - `createServer(config, composition?)` now accepts an optional composition object. - **Breaking (MCP):** Experimental tool response fields are nested under `experimental` on success payloads. Affected tools: `query`, `query_documents`, `guided_query`. Fields moved: `degraded`, `degradation_reason`, `hybrid_leg_failed`, `rerank_skipped_reason` (query-shaped tools); `decision_trace` (`guided_query`). Stable fields (`status`, `results`, `namespace`, etc.) are unchanged. See [MIGRATION.md](docs/MIGRATION.md#unreleased-stable-vs-experimental-response-fields). - **Breaking (core):** `resolveConfig` requires a Pinecone index name and no longer applies Alliance index/rerank defaults. Removed exported `DEFAULT_INDEX_NAME` and `DEFAULT_RERANK_MODEL` from the package root. Rerank is opt-in when `PINECONE_RERANK_MODEL` / `rerankModel` is unset. -- **Breaking (core):** `setupCoreServer` MCP `instructions` use `CORE_SERVER_INSTRUCTIONS` (no `guided_query` / `suggest_query_params`). `resolveConfig` defaults `disableSuggestFlow` to `true` so `query` / `count` / `query_documents` work without Alliance tools. Alliance CLI / `resolveAllianceConfig` unchanged: gate on by default, `ALLIANCE_SERVER_INSTRUCTIONS`. +- **Breaking (core):** `setupCoreServer` MCP `instructions` use `CORE_SERVER_INSTRUCTIONS` (includes `guided_query`; no `suggest_query_params`). `resolveConfig` defaults `disableSuggestFlow` to `true` so `query` / `count` / `query_documents` work without Alliance tools. Alliance CLI / `resolveAllianceConfig` unchanged: gate on by default, `ALLIANCE_SERVER_INSTRUCTIONS`. - **Alliance CLI / `resolveAllianceConfig`:** When index or rerank env/CLI values are omitted, defaults remain `rag-hybrid` and `bge-reranker-v2-m3` (API-key-only MCP configs unchanged). See [examples/alliance/.env.example](examples/alliance/.env.example). ## [0.2.0] - 2026-05-29 diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d113b08..d030944 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -37,6 +37,8 @@ For the full Alliance tool surface (including `suggest_query_params`, `guided_qu | `resolveConfig` | Package root, `setupCoreServer`, quickstart | **Throws** | Omitted (no rerank) | Off (`disableSuggestFlow: true`) | | `resolveAllianceConfig` | Published CLI, `setupAllianceServer` | `rag-hybrid` | `bge-reranker-v2-m3` | On (`disableSuggestFlow: false`) | +**Warning:** Switching between `resolveConfig` / `setupCoreServer` (package root) and `resolveAllianceConfig` / `setupAllianceServer` changes suggest-flow gate behavior. Core defaults bypass the gate; Alliance defaults enforce it. Use `guided_query` (registered in both setups) for single-call retrieval without manual `suggest_query_params`, or align `disableSuggestFlow` explicitly when migrating between entry points. + C++ Alliance deployers can copy [examples/alliance/.env.example](../examples/alliance/.env.example). Constants: `ALLIANCE_DEFAULT_INDEX_NAME` / `ALLIANCE_DEFAULT_RERANK_MODEL` from `@will-cppa/pinecone-read-only-mcp/alliance`. --- diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 9881a35..fbd0bb7 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -226,6 +226,17 @@ Core `resolveConfig` throws `Missing Pinecone index name: …` when the index is - The binary uses `resolveAllianceConfig`; API-key-only configs still default to `rag-hybrid` and `bge-reranker-v2-m3`. - Explicit env overrides still win. Copy [examples/alliance/.env.example](../examples/alliance/.env.example) to document Alliance conventions. +### Suggest-flow gate (`disableSuggestFlow`) — intentional divergence + +| Entry point | Default | Rationale | +| ----------- | ------- | --------- | +| `resolveConfig` / `setupCoreServer` | Gate **off** (`disableSuggestFlow: true`) | Generic embedders can call `query` / `count` / `query_documents` directly; `guided_query` is the recommended ceremony-free path | +| `resolveAllianceConfig` / CLI | Gate **on** (`disableSuggestFlow: false`) | Production MCP safety: manual `query` requires `suggest_query_params` first | + +**Decision (unchanged):** Exposing `guided_query` in core does **not** change the core gate default. Core keeps the gate off so direct tool access remains available; Alliance keeps the gate on for CLI/MCP deployments. + +**When migrating entry points:** If you move from Alliance to core (or vice versa), query gate behavior changes unless you set `disableSuggestFlow` explicitly. Use `guided_query` to avoid the multi-step suggest flow in either setup. + --- ## Migrating to v0.2.0 diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 0ca6790..cd28e82 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -24,8 +24,8 @@ Promotion process: [deprecation-policy.md § Stable vs experimental](./deprecati | Setup | Tools | MCP instructions | | ----- | ----- | ------------------ | -| `setupCoreServer` (package root) | **7:** `list_namespaces`, `namespace_router`, `count`, `query`, `keyword_search`, `query_documents`, `generate_urls` | `CORE_SERVER_INSTRUCTIONS` — no `guided_query` or `suggest_query_params` | -| `setupAllianceServer` / published CLI | **9:** core tools plus `suggest_query_params`, `guided_query` | `ALLIANCE_SERVER_INSTRUCTIONS` — includes guided/suggest quickstart | +| `setupCoreServer` (package root) | **8:** `list_namespaces`, `namespace_router`, `count`, `query`, `keyword_search`, `query_documents`, `generate_urls`, `guided_query` | `CORE_SERVER_INSTRUCTIONS` — includes `guided_query`; no `suggest_query_params` | +| `setupAllianceServer` / published CLI | **9:** core tools plus `suggest_query_params` | `ALLIANCE_SERVER_INSTRUCTIONS` — includes suggest-flow quickstart | ## Suggest-flow gate diff --git a/src/__tests__/cross-entry-point.test.ts b/src/__tests__/cross-entry-point.test.ts new file mode 100644 index 0000000..8dc1c38 --- /dev/null +++ b/src/__tests__/cross-entry-point.test.ts @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { resolveAllianceConfig } from '../alliance/config.js'; +import { resolveConfig } from '../core/config.js'; +import * as guidedQueryTool from '../core/server/tools/guided-query-tool.js'; +import { registerQueryTool } from '../core/server/tools/query-tool.js'; +import { + assertToolErrorCode, + createMockServer, + createTestServerContext, + makeHybridQueryResult, + parseToolJson, +} from '../core/server/tools/test-helpers.js'; +import { setupCoreServer, teardownServer } from '../core/setup.js'; +import { PineconeClient } from '../core/pinecone-client.js'; + +describe('cross-entry-point: core vs Alliance defaults', () => { + afterEach(() => { + teardownServer(); + }); + + it('core entry point disables suggest-flow gate by default', () => { + const cfg = resolveConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + expect(cfg.disableSuggestFlow).toBe(true); + }); + + it('Alliance entry point enables suggest-flow gate by default', () => { + const cfg = resolveAllianceConfig({ apiKey: 'sk-test' }, { PINECONE_API_KEY: 'sk-test' }); + expect(cfg.disableSuggestFlow).toBe(false); + }); + + it('core-initialized server: query succeeds without prior suggest_query_params', async () => { + const query = vi.fn().mockResolvedValue(makeHybridQueryResult()); + const ctx = createTestServerContext({ + config: { + disableSuggestFlow: true, + }, + client: { query } as never, + }); + const server = createMockServer(); + registerQueryTool(server as never, ctx); + const body = parseToolJson( + await server.getHandler('query')!({ + query_text: 'hello', + namespace: 'wg21', + preset: 'fast', + }) + ); + expect(body.status).toBe('success'); + expect(query).toHaveBeenCalledOnce(); + }); + + it('Alliance-initialized server: query returns FLOW_GATE without prior suggestion', async () => { + const cfg = resolveAllianceConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + const ctx = createTestServerContext({ + config: cfg, + client: { query: vi.fn() } as never, + }); + const server = createMockServer(); + registerQueryTool(server as never, ctx); + const raw = await server.getHandler('query')!({ + query_text: 'hello', + namespace: 'wg21', + preset: 'fast', + }); + const err = assertToolErrorCode(raw, 'FLOW_GATE'); + expect(err.suggestion).toBe("Call suggest_query_params for namespace 'wg21' first"); + }); + + it('core setup registers guided_query handler', async () => { + const cfg = resolveConfig({ apiKey: 'sk-cross', indexName: 'test-index' }); + const ctx = createTestServerContext({ + config: cfg, + client: new PineconeClient({ + apiKey: cfg.apiKey, + indexName: cfg.indexName, + defaultTopK: cfg.defaultTopK, + }), + }); + const registerSpy = vi.spyOn(guidedQueryTool, 'registerGuidedQueryTool'); + + await setupCoreServer({ context: ctx }); + + expect(registerSpy).toHaveBeenCalledOnce(); + expect(ctx.hasToolsRegistered()).toBe(true); + registerSpy.mockRestore(); + }); +}); diff --git a/src/alliance/config.ts b/src/alliance/config.ts index 294965f..10a0d3d 100644 --- a/src/alliance/config.ts +++ b/src/alliance/config.ts @@ -23,6 +23,13 @@ export const DEFAULT_ALLIANCE_RERANK_MODEL = ALLIANCE_DEFAULT_RERANK_MODEL; * Build {@link ServerConfig} 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 + * `false` (gate on) unless `PINECONE_DISABLE_SUGGEST_FLOW` or `disableSuggestFlow` in overrides + * says otherwise. This differs from package-root {@link resolveConfig} (`true` / gate off). + * Switching between core and Alliance entry points changes query gate behavior — use + * `guided_query` (available in both) for ceremony-free retrieval, or call + * `suggest_query_params` before gated tools when the gate is on. + * * Output is the `config` half of the embedder pattern `{ config, composition }`. * Pair with {@link createIsolatedContext} or {@link createServer} and an optional * {@link ServerContextComposition} for per-instance injectables. diff --git a/src/alliance/setup.ts b/src/alliance/setup.ts index cd36f13..cc4c030 100644 --- a/src/alliance/setup.ts +++ b/src/alliance/setup.ts @@ -4,7 +4,6 @@ import { getDefaultServerContext, type ServerContext } from '../core/server/serv import { resolveAllianceConfig } from './config.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'; /** @@ -56,7 +55,7 @@ function normalizeSetupAllianceArgs( /** * 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. + * all core tools (including `guided_query`) plus `suggest_query_params` and built-in URL generators. * * When `config` is omitted, resolves env via {@link resolveAllianceConfig} (Alliance index/rerank defaults when unset). */ @@ -88,6 +87,5 @@ export async function setupAllianceServer( registerBuiltinUrlGenerators(resolvedCtx); registerSuggestQueryParamsTool(server, resolvedCtx); - registerGuidedQueryTool(server, resolvedCtx); return server; } diff --git a/src/alliance/tools/guided-query-tool.ts b/src/alliance/tools/guided-query-tool.ts index a6c2347..c3fa890 100644 --- a/src/alliance/tools/guided-query-tool.ts +++ b/src/alliance/tools/guided-query-tool.ts @@ -1,272 +1 @@ -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import { guidedRerankStatus } from '../../core/rerank-trace.js'; -import { FAST_QUERY_FIELDS, MAX_TOP_K, MIN_TOP_K } from '../../constants.js'; -import { getPineconeClient } from '../../core/server/client-context.js'; -import { formatQueryResultRows } from '../../core/server/format-query-result.js'; -import { - metadataFilterSchema, - validateMetadataFilterDetailed, -} from '../../core/server/metadata-filter.js'; -import { rankNamespacesByQuery } from '../../core/server/namespace-router.js'; -import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; -import { normalizeNamespace } from '../../core/server/namespace-utils.js'; -import type { ServerContext } from '../../core/server/server-context.js'; -import { suggestQueryParams } from '../../core/server/query-suggestion.js'; -import { markSuggested } from '../../core/server/suggestion-flow.js'; -import { - classifyToolCatchError, - lifecycleToolError, - logToolError, - pineconeToolError, - validationToolError, -} from '../../core/server/tool-error.js'; -import { - buildGuidedQueryExperimental, - buildQueryExperimental, - guidedQueryResponseSchema, - type GuidedQueryResponse, - type QueryResponse, -} from '../../core/server/response-schemas.js'; -import { jsonErrorResponse, validatedJsonResponse } from '../../core/server/tool-response.js'; - -type GuidedToolName = 'count' | 'fast' | 'detailed' | 'full'; - -function resolveGuidedToolName( - preferred: 'auto' | 'count' | 'fast' | 'detailed' | 'full', - suggestion: { recommended_tool: GuidedToolName } -): GuidedToolName { - if (preferred === 'auto') return suggestion.recommended_tool; - if (preferred === 'count') return 'count'; - if (preferred === 'fast') return 'fast'; - if (preferred === 'detailed') return 'detailed'; - return 'full'; -} - -/** - * Registers `guided_query` (routing + suggestion + execution in one call). - * See "Retrieval tool decision matrix" in README.md for tool-selection guidance. - */ -export function registerGuidedQueryTool(server: McpServer, ctx?: ServerContext): void { - server.registerTool( - 'guided_query', - { - description: - 'Combines namespace routing, suggestion, and query into a single call — no prerequisite tools needed. ' + - 'Single orchestrator: optional namespace_router logic -> executes count or hybrid query (fast / detailed / full presets). ' + - 'Returns experimental.decision_trace so behavior stays transparent and debuggable.', - inputSchema: { - user_query: z.string().describe('User question or intent.'), - namespace: z - .string() - .optional() - .describe( - 'Optional explicit namespace. If omitted, namespace_router logic will choose one.' - ), - metadata_filter: metadataFilterSchema - .optional() - .describe('Optional metadata filter to constrain results.'), - top_k: z - .number() - .int() - .min(MIN_TOP_K) - .max(MAX_TOP_K) - .default(10) - .describe('Result count for hybrid query paths (1-100).'), - preferred_tool: z - .enum(['auto', 'count', 'fast', 'detailed', 'full']) - .default('auto') - .describe( - 'Optional override: count, fast (no rerank / light fields), detailed (reranked), full (explicit rerank + fields), or auto from suggestion.' - ), - enrich_urls: z - .boolean() - .default(true) - .describe( - 'If true, enrich result URLs using the namespace URL generator when metadata.url is missing (if supported for that namespace).' - ), - }, - }, - async (params) => { - try { - if (ctx?.disposed) { - return jsonErrorResponse(lifecycleToolError('ServerContext has been disposed')); - } - const { - user_query, - namespace: inputNamespace, - metadata_filter, - top_k, - preferred_tool, - enrich_urls, - } = params; - - if (!user_query?.trim()) { - return jsonErrorResponse(validationToolError('user_query cannot be empty', 'user_query')); - } - - if (metadata_filter) { - const err = validateMetadataFilterDetailed(metadata_filter); - if (err) { - return jsonErrorResponse(validationToolError(err.message, err.field)); - } - } - - const queryText = user_query.trim(); - const { data: namespaces, cache_hit } = ctx - ? await ctx.getNamespacesWithCache() - : await getNamespacesWithCache(); - const ranked = rankNamespacesByQuery(queryText, namespaces, 3); - - let namespace: string | null = null; - if (inputNamespace !== undefined) { - namespace = normalizeNamespace(inputNamespace); - if (!namespace) { - return jsonErrorResponse(validationToolError('namespace cannot be empty', 'namespace')); - } - } else { - const top = ranked[0]?.namespace; - namespace = top ? normalizeNamespace(top) : null; - } - /* - * ToolError mapping: empty index / no routable namespace is backend/data state - * (PINECONE_ERROR, recoverable). Explicit namespace missing from cache is user/input - * (VALIDATION, field namespace). - */ - if (!namespace) { - return jsonErrorResponse( - pineconeToolError( - 'No namespace available. Please run list_namespaces and verify index data.', - { - recoverable: true, - suggestion: 'Call list_namespaces to confirm the index has namespaces, then retry.', - } - ) - ); - } - - const ns = namespaces.find( - (n) => n.namespace === namespace || normalizeNamespace(n.namespace) === namespace - ); - const suggestion = suggestQueryParams(ns?.metadata ?? null, queryText); - if (!suggestion.namespace_found) { - return jsonErrorResponse( - validationToolError( - `Namespace "${namespace}" not found in cached namespaces. Call list_namespaces and retry.`, - 'namespace', - { - suggestion: - 'Use a namespace name returned by list_namespaces, then call list_namespaces again if the cache is stale.', - } - ) - ); - } - - const selectedTool: GuidedToolName = resolveGuidedToolName(preferred_tool, suggestion); - if (ctx) { - ctx.markSuggested(namespace, { - recommended_tool: selectedTool, - suggested_fields: suggestion.suggested_fields, - user_query: queryText, - }); - } else { - markSuggested(namespace, { - recommended_tool: selectedTool, - suggested_fields: suggestion.suggested_fields, - user_query: queryText, - }); - } - - const client = ctx ? ctx.getClient() : getPineconeClient(); - const enrichUrls = enrich_urls ?? true; - const baseTrace = { - cache_hit, - input_namespace: inputNamespace ?? null, - routed_namespace: ranked[0]?.namespace ?? null, - selected_namespace: namespace, - ranked_namespaces: ranked, - suggested_fields: suggestion.suggested_fields, - suggested_tool: suggestion.recommended_tool, - selected_tool: selectedTool, - explanation: suggestion.explanation, - enrich_urls: enrichUrls, - }; - - if (selectedTool === 'count') { - const { count, truncated } = await client.count({ - query: queryText, - namespace, - metadataFilter: metadata_filter, - }); - const countResponse: GuidedQueryResponse = { - status: 'success', - ...buildGuidedQueryExperimental({ - ...baseTrace, - rerank_status: 'skipped' as const, - }), - result: { - tool: 'count', - namespace, - query: queryText, - ...(metadata_filter !== undefined ? { metadata_filter } : {}), - count, - truncated, - }, - }; - return validatedJsonResponse(guidedQueryResponseSchema, countResponse); - } - - const isFast = selectedTool === 'fast'; - const mode: QueryResponse['mode'] = - selectedTool === 'fast' - ? 'query_fast' - : selectedTool === 'detailed' - ? 'query_detailed' - : 'query'; - const fields = - suggestion.suggested_fields.length > 0 - ? suggestion.suggested_fields - : isFast - ? [...FAST_QUERY_FIELDS] - : undefined; - const queryOutcome = await client.query({ - query: queryText, - namespace, - topK: top_k, - metadataFilter: metadata_filter, - useReranking: !isFast, - fields: fields?.length ? fields : undefined, - }); - const rerank_status = guidedRerankStatus(!isFast, queryOutcome); - const formattedResults = formatQueryResultRows(queryOutcome.results, { - namespace, - enrichUrls: enrichUrls, - ctx, - }); - const result: QueryResponse = { - status: 'success', - mode, - query: queryText, - namespace, - metadata_filter: metadata_filter, - result_count: formattedResults.length, - ...(fields?.length ? { fields } : {}), - results: formattedResults, - ...buildQueryExperimental(queryOutcome), - }; - const queryResponse: GuidedQueryResponse = { - status: 'success', - ...buildGuidedQueryExperimental({ - ...baseTrace, - rerank_status, - }), - result, - }; - return validatedJsonResponse(guidedQueryResponseSchema, queryResponse); - } catch (error) { - logToolError('guided_query', error); - return jsonErrorResponse(classifyToolCatchError(error, 'Failed to execute guided query')); - } - } - ); -} +export { registerGuidedQueryTool } from '../../core/server/tools/guided-query-tool.js'; diff --git a/src/constants.test.ts b/src/constants.test.ts index fc85d3c..432c962 100644 --- a/src/constants.test.ts +++ b/src/constants.test.ts @@ -6,8 +6,8 @@ import { } from './constants.js'; describe('server instructions', () => { - it('CORE_SERVER_INSTRUCTIONS does not reference Alliance-only tools', () => { - expect(CORE_SERVER_INSTRUCTIONS).not.toMatch(/guided_query/); + it('CORE_SERVER_INSTRUCTIONS includes guided_query but not suggest_query_params', () => { + expect(CORE_SERVER_INSTRUCTIONS).toMatch(/guided_query/); expect(CORE_SERVER_INSTRUCTIONS).not.toMatch(/suggest_query_params/); }); diff --git a/src/constants.ts b/src/constants.ts index 1d5476f..51ff196 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -49,15 +49,16 @@ Notes: - Result rows include both \`document_id\` (canonical) and \`paper_number\` (deprecated alias kept for one minor cycle; will be removed in the next major release). Prefer \`document_id\` in new code. - The server emits structured logs to stderr (text by default, set PINECONE_READ_ONLY_MCP_LOG_FORMAT=json for log aggregation).`; -/** MCP instructions for {@link setupCoreServer} (seven core tools only). */ -export const CORE_SERVER_INSTRUCTIONS = `Quickstart for AI clients: call \`list_namespaces\` to discover namespaces, optionally \`namespace_router\` to rank candidates from user intent, then \`query\` (preset fast/detailed/full), \`count\`, \`query_documents\`, \`keyword_search\`, or \`generate_urls\` as needed. For \`setupCoreServer\` / package-root \`resolveConfig\`, \`PINECONE_INDEX_NAME\` (or \`--index-name\`) is required in addition to \`PINECONE_API_KEY\`. Misconfiguration surfaces at startup, not as tool errors. +/** MCP instructions for {@link setupCoreServer} (eight core tools including guided_query). */ +export const CORE_SERVER_INSTRUCTIONS = `Quickstart for AI clients: for most user questions, call \`guided_query\` with the user's question — it does namespace routing, suggestion, and execution in one shot and returns \`experimental.decision_trace\` you can show the user. Alternatively call \`list_namespaces\` to discover namespaces, optionally \`namespace_router\` to rank candidates from user intent, then \`query\` (preset fast/detailed/full), \`count\`, \`query_documents\`, \`keyword_search\`, or \`generate_urls\` as needed. For \`setupCoreServer\` / package-root \`resolveConfig\`, \`PINECONE_INDEX_NAME\` (or \`--index-name\`) is required in addition to \`PINECONE_API_KEY\`. Misconfiguration surfaces at startup, not as tool errors. ${SERVER_FEATURES_AND_NOTES} Usage: -1. Use list_namespaces (cached) to discover available namespaces in the index. The response includes \`expires_at_iso\` so you know when to refresh. -2. Optionally use namespace_router to choose candidate namespace(s) from user intent. -3. Use count for count questions, \`query\` with the appropriate preset for chunk-level retrieval, query_documents for full-document content, keyword_search for lexical retrieval, or generate_urls when records need synthesized URLs.`; +1. Prefer guided_query for single-call retrieval (no prerequisite tools). +2. Use list_namespaces (cached) to discover available namespaces in the index. The response includes \`expires_at_iso\` so you know when to refresh. +3. Optionally use namespace_router to choose candidate namespace(s) from user intent. +4. Use count for count questions, \`query\` with the appropriate preset for chunk-level retrieval, query_documents for full-document content, keyword_search for lexical retrieval, or generate_urls when records need synthesized URLs.`; /** Alliance-only supplement appended to core instructions for {@link setupAllianceServer}. */ export const ALLIANCE_INSTRUCTIONS_APPENDIX = ` diff --git a/src/core/config.ts b/src/core/config.ts index e983474..54d48a4 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -42,7 +42,12 @@ export interface ServerConfig { cacheTtlMs: number; /** Per-call timeout (ms) applied to outbound Pinecone requests. */ requestTimeoutMs: number; - /** When true, the suggest_query_params flow gate is bypassed. */ + /** + * When true, the suggest_query_params flow gate is bypassed for `query`, `count`, and + * `query_documents`. Core {@link resolveConfig} defaults this to `true` so generic embedders + * can call retrieval tools directly; use `guided_query` for ceremony-free orchestration or + * set `PINECONE_DISABLE_SUGGEST_FLOW=false` to require `suggest_query_params` first. + */ disableSuggestFlow: boolean; /** When true, on-startup probe verifies dense + sparse indexes exist. */ checkIndexes: boolean; @@ -108,6 +113,12 @@ export interface ConfigOverrides { * passed to {@link createIsolatedContext} (multi-tenant) or {@link createServer} * (singleton CLI path). * + * **Suggest-flow gate default:** `disableSuggestFlow` defaults to `true` (gate off). Generic + * embedders can call `query` / `count` / `query_documents` without `suggest_query_params`; + * prefer `guided_query` for single-call orchestration. Set `PINECONE_DISABLE_SUGGEST_FLOW=false` + * or `disableSuggestFlow: false` in overrides to enable the gate. Alliance + * {@link resolveAllianceConfig} overrides this to `false` by default. + * * @throws Error when no API key or index name is provided. */ export function resolveConfig( diff --git a/src/core/server/redaction.test.ts b/src/core/server/redaction.test.ts index 332c875..d97e616 100644 --- a/src/core/server/redaction.test.ts +++ b/src/core/server/redaction.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getPineconeClient } from './client-context.js'; import { getNamespacesWithCache } from './namespaces-cache.js'; -import { registerGuidedQueryTool } from '../../alliance/tools/guided-query-tool.js'; +import { registerGuidedQueryTool } from './tools/guided-query-tool.js'; import { setLogLevel } from '../../logger.js'; import { redactApiKey, redactSensitiveFields } from '../../logger.js'; import { classifyToolCatchError, pineconeToolError } from './tool-error.js'; diff --git a/src/alliance/tools/guided-query-tool.context.test.ts b/src/core/server/tools/guided-query-tool.context.test.ts similarity index 95% rename from src/alliance/tools/guided-query-tool.context.test.ts rename to src/core/server/tools/guided-query-tool.context.test.ts index 5948dfe..520e406 100644 --- a/src/alliance/tools/guided-query-tool.context.test.ts +++ b/src/core/server/tools/guided-query-tool.context.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; -import { registerBuiltinUrlGenerators } from '../url-builtins.js'; -import { registerQueryTool } from '../../core/server/tools/query-tool.js'; -import { registerSuggestQueryParamsTool } from './suggest-query-params-tool.js'; +import { registerBuiltinUrlGenerators } from '../../../alliance/url-builtins.js'; +import { guidedQueryResponseSchema } from '../response-schemas.js'; +import { registerQueryTool } from './query-tool.js'; +import { registerSuggestQueryParamsTool } from '../../../alliance/tools/suggest-query-params-tool.js'; import { registerGuidedQueryTool } from './guided-query-tool.js'; -import { guidedQueryResponseSchema } from '../../core/server/response-schemas.js'; import { assertToolErrorCode, createMockServer, @@ -12,7 +12,7 @@ import { makeHybridQueryResult, makeSearchResult, parseToolJson, -} from '../../core/server/tools/test-helpers.js'; +} from './test-helpers.js'; const namespaceMetadata = { document_number: 'string', diff --git a/src/alliance/tools/guided-query-tool.test.ts b/src/core/server/tools/guided-query-tool.test.ts similarity index 94% rename from src/alliance/tools/guided-query-tool.test.ts rename to src/core/server/tools/guided-query-tool.test.ts index d39f614..b43c4ab 100644 --- a/src/alliance/tools/guided-query-tool.test.ts +++ b/src/core/server/tools/guided-query-tool.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getPineconeClient } from '../../core/server/client-context.js'; -import { getNamespacesWithCache } from '../../core/server/namespaces-cache.js'; +import { getPineconeClient } from '../client-context.js'; +import { getNamespacesWithCache } from '../namespaces-cache.js'; import { registerGuidedQueryTool } from './guided-query-tool.js'; import { assertToolErrorCode, @@ -9,18 +9,18 @@ import { makeNamespaceCacheEntry, makeSearchResult, parseToolJson, -} from '../../core/server/tools/test-helpers.js'; +} from './test-helpers.js'; -vi.mock('../../core/server/client-context.js', () => ({ +vi.mock('../client-context.js', () => ({ getPineconeClient: vi.fn(), })); -vi.mock('../../core/server/namespaces-cache.js', () => ({ +vi.mock('../namespaces-cache.js', () => ({ getNamespacesWithCache: vi.fn(), })); /** Real `markSuggested` may call `getServerConfig()` during sweep (CI has no API key); isolate the handler. */ -vi.mock('../../core/server/suggestion-flow.js', () => ({ +vi.mock('../suggestion-flow.js', () => ({ markSuggested: vi.fn(), })); diff --git a/src/core/server/tools/guided-query-tool.ts b/src/core/server/tools/guided-query-tool.ts new file mode 100644 index 0000000..0528e17 --- /dev/null +++ b/src/core/server/tools/guided-query-tool.ts @@ -0,0 +1,269 @@ +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 { guidedRerankStatus } from '../../rerank-trace.js'; +import { getPineconeClient } from '../client-context.js'; +import { formatQueryResultRows } from '../format-query-result.js'; +import { metadataFilterSchema, validateMetadataFilterDetailed } from '../metadata-filter.js'; +import { rankNamespacesByQuery } from '../namespace-router.js'; +import { getNamespacesWithCache } from '../namespaces-cache.js'; +import { normalizeNamespace } from '../namespace-utils.js'; +import { suggestQueryParams } from '../query-suggestion.js'; +import type { ServerContext } from '../server-context.js'; +import { + buildGuidedQueryExperimental, + buildQueryExperimental, + guidedQueryResponseSchema, + type GuidedQueryResponse, + type QueryResponse, +} from '../response-schemas.js'; +import { markSuggested } from '../suggestion-flow.js'; +import { + classifyToolCatchError, + lifecycleToolError, + logToolError, + pineconeToolError, + validationToolError, +} from '../tool-error.js'; +import { jsonErrorResponse, validatedJsonResponse } from '../tool-response.js'; + +type GuidedToolName = 'count' | 'fast' | 'detailed' | 'full'; + +function resolveGuidedToolName( + preferred: 'auto' | 'count' | 'fast' | 'detailed' | 'full', + suggestion: { recommended_tool: GuidedToolName } +): GuidedToolName { + if (preferred === 'auto') return suggestion.recommended_tool; + if (preferred === 'count') return 'count'; + if (preferred === 'fast') return 'fast'; + if (preferred === 'detailed') return 'detailed'; + return 'full'; +} + +/** + * Registers `guided_query` (routing + suggestion + execution in one call). + * See "Retrieval tool decision matrix" in README.md for tool-selection guidance. + */ +export function registerGuidedQueryTool(server: McpServer, ctx?: ServerContext): void { + server.registerTool( + 'guided_query', + { + description: + 'Combines namespace routing, suggestion, and query into a single call — no prerequisite tools needed. ' + + 'Single orchestrator: optional namespace_router logic -> executes count or hybrid query (fast / detailed / full presets). ' + + 'Returns experimental.decision_trace so behavior stays transparent and debuggable.', + inputSchema: { + user_query: z.string().describe('User question or intent.'), + namespace: z + .string() + .optional() + .describe( + 'Optional explicit namespace. If omitted, namespace_router logic will choose one.' + ), + metadata_filter: metadataFilterSchema + .optional() + .describe('Optional metadata filter to constrain results.'), + top_k: z + .number() + .int() + .min(MIN_TOP_K) + .max(MAX_TOP_K) + .default(10) + .describe('Result count for hybrid query paths (1-100).'), + preferred_tool: z + .enum(['auto', 'count', 'fast', 'detailed', 'full']) + .default('auto') + .describe( + 'Optional override: count, fast (no rerank / light fields), detailed (reranked), full (explicit rerank + fields), or auto from suggestion.' + ), + enrich_urls: z + .boolean() + .default(true) + .describe( + 'If true, enrich result URLs using the namespace URL generator when metadata.url is missing (if supported for that namespace).' + ), + }, + }, + async (params) => { + try { + if (ctx?.disposed) { + return jsonErrorResponse(lifecycleToolError('ServerContext has been disposed')); + } + const { + user_query, + namespace: inputNamespace, + metadata_filter, + top_k, + preferred_tool, + enrich_urls, + } = params; + + if (!user_query?.trim()) { + return jsonErrorResponse(validationToolError('user_query cannot be empty', 'user_query')); + } + + if (metadata_filter) { + const err = validateMetadataFilterDetailed(metadata_filter); + if (err) { + return jsonErrorResponse(validationToolError(err.message, err.field)); + } + } + + const queryText = user_query.trim(); + const { data: namespaces, cache_hit } = ctx + ? await ctx.getNamespacesWithCache() + : await getNamespacesWithCache(); + const ranked = rankNamespacesByQuery(queryText, namespaces, 3); + + let namespace: string | null = null; + if (inputNamespace !== undefined) { + namespace = normalizeNamespace(inputNamespace); + if (!namespace) { + return jsonErrorResponse(validationToolError('namespace cannot be empty', 'namespace')); + } + } else { + const top = ranked[0]?.namespace; + namespace = top ? normalizeNamespace(top) : null; + } + /* + * ToolError mapping: empty index / no routable namespace is backend/data state + * (PINECONE_ERROR, recoverable). Explicit namespace missing from cache is user/input + * (VALIDATION, field namespace). + */ + if (!namespace) { + return jsonErrorResponse( + pineconeToolError( + 'No namespace available. Please run list_namespaces and verify index data.', + { + recoverable: true, + suggestion: 'Call list_namespaces to confirm the index has namespaces, then retry.', + } + ) + ); + } + + const ns = namespaces.find( + (n) => n.namespace === namespace || normalizeNamespace(n.namespace) === namespace + ); + const suggestion = suggestQueryParams(ns?.metadata ?? null, queryText); + if (!suggestion.namespace_found) { + return jsonErrorResponse( + validationToolError( + `Namespace "${namespace}" not found in cached namespaces. Call list_namespaces and retry.`, + 'namespace', + { + suggestion: + 'Use a namespace name returned by list_namespaces, then call list_namespaces again if the cache is stale.', + } + ) + ); + } + + const selectedTool: GuidedToolName = resolveGuidedToolName(preferred_tool, suggestion); + if (ctx) { + ctx.markSuggested(namespace, { + recommended_tool: selectedTool, + suggested_fields: suggestion.suggested_fields, + user_query: queryText, + }); + } else { + markSuggested(namespace, { + recommended_tool: selectedTool, + suggested_fields: suggestion.suggested_fields, + user_query: queryText, + }); + } + + const client = ctx ? ctx.getClient() : getPineconeClient(); + const enrichUrls = enrich_urls ?? true; + const baseTrace = { + cache_hit, + input_namespace: inputNamespace ?? null, + routed_namespace: ranked[0]?.namespace ?? null, + selected_namespace: namespace, + ranked_namespaces: ranked, + suggested_fields: suggestion.suggested_fields, + suggested_tool: suggestion.recommended_tool, + selected_tool: selectedTool, + explanation: suggestion.explanation, + enrich_urls: enrichUrls, + }; + + if (selectedTool === 'count') { + const { count, truncated } = await client.count({ + query: queryText, + namespace, + metadataFilter: metadata_filter, + }); + const countResponse: GuidedQueryResponse = { + status: 'success', + ...buildGuidedQueryExperimental({ + ...baseTrace, + rerank_status: 'skipped' as const, + }), + result: { + tool: 'count', + namespace, + query: queryText, + ...(metadata_filter !== undefined ? { metadata_filter } : {}), + count, + truncated, + }, + }; + return validatedJsonResponse(guidedQueryResponseSchema, countResponse); + } + + const isFast = selectedTool === 'fast'; + const mode: QueryResponse['mode'] = + selectedTool === 'fast' + ? 'query_fast' + : selectedTool === 'detailed' + ? 'query_detailed' + : 'query'; + const fields = + suggestion.suggested_fields.length > 0 + ? suggestion.suggested_fields + : isFast + ? [...FAST_QUERY_FIELDS] + : undefined; + const queryOutcome = await client.query({ + query: queryText, + namespace, + topK: top_k, + metadataFilter: metadata_filter, + useReranking: !isFast, + fields: fields?.length ? fields : undefined, + }); + const rerank_status = guidedRerankStatus(!isFast, queryOutcome); + const formattedResults = formatQueryResultRows(queryOutcome.results, { + namespace, + enrichUrls: enrichUrls, + ctx, + }); + const result: QueryResponse = { + status: 'success', + mode, + query: queryText, + namespace, + metadata_filter: metadata_filter, + result_count: formattedResults.length, + ...(fields?.length ? { fields } : {}), + results: formattedResults, + ...buildQueryExperimental(queryOutcome), + }; + const queryResponse: GuidedQueryResponse = { + status: 'success', + ...buildGuidedQueryExperimental({ + ...baseTrace, + rerank_status, + }), + result, + }; + return validatedJsonResponse(guidedQueryResponseSchema, queryResponse); + } catch (error) { + logToolError('guided_query', error); + return jsonErrorResponse(classifyToolCatchError(error, 'Failed to execute guided query')); + } + } + ); +} diff --git a/src/core/setup.ts b/src/core/setup.ts index 08e2e89..63ebc67 100644 --- a/src/core/setup.ts +++ b/src/core/setup.ts @@ -10,6 +10,7 @@ import { } from './server/server-context.js'; import { registerCountTool } from './server/tools/count-tool.js'; import { registerGenerateUrlsTool } from './server/tools/generate-urls-tool.js'; +import { registerGuidedQueryTool } from './server/tools/guided-query-tool.js'; import { registerKeywordSearchTool } from './server/tools/keyword-search-tool.js'; import { registerListNamespacesTool } from './server/tools/list-namespaces-tool.js'; import { registerNamespaceRouterTool } from './server/tools/namespace-router-tool.js'; @@ -29,11 +30,12 @@ export function teardownServer(): void { } /** - * Create and configure the MCP server with generic (core) tools only. + * Create and configure the MCP server with generic (core) tools. * - * Does not register Alliance-specific tools (`suggest_query_params`, `guided_query`) - * or built-in Boost/Slack URL generators. Use {@link setupAllianceServer} from - * `@will-cppa/pinecone-read-only-mcp/alliance` for the full tool surface. + * Registers eight core tools including `guided_query`. Does not register Alliance-specific + * `suggest_query_params` or built-in Boost/Slack URL generators. Use + * {@link setupAllianceServer} from `@will-cppa/pinecone-read-only-mcp/alliance` for the + * full Alliance tool surface (suggest-flow gate on by default). */ export type SetupCoreServerOptions = { config?: ServerConfig; @@ -138,6 +140,7 @@ export async function setupCoreServer( registerKeywordSearchTool(server, ctx); registerQueryDocumentsTool(server, ctx); registerGenerateUrlsTool(server, ctx); + registerGuidedQueryTool(server, ctx); ctx.markToolsRegistered(); From 98dc5c26b47fa0ad0da1c140b80bbff66065da9d Mon Sep 17 00:00:00 2001 From: zho Date: Wed, 17 Jun 2026 01:40:48 +0800 Subject: [PATCH 2/2] addressed ai agent reviews --- README.md | 4 ++-- src/__tests__/cross-entry-point.test.ts | 29 +++++++++++++++++++++++++ src/constants.test.ts | 9 ++++++++ src/constants.ts | 2 +- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 079ff92..7c98c62 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ 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 use `createServer(config)` → `ctx.setClient(...)` → `setupAllianceServer({ context: ctx })` for the full tool surface (what the CLI uses); see [Library embedding](#library-embedding) below. +- **`src/core/`** — generic MCP–Pinecone bridge (`PineconeClient`, `resolveConfig`, eight MCP tools including `guided_query`). Import from `@will-cppa/pinecone-read-only-mcp` (package root). +- **`src/alliance/`** — C++ Alliance app layer (`suggest_query_params`, Boost/Slack URL builtins). Import from `@will-cppa/pinecone-read-only-mcp/alliance` and use `createServer(config)` → `ctx.setClient(...)` → `setupAllianceServer({ context: ctx })` for the full tool surface (what the CLI uses); suggest-flow gate on by default. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for core vs Alliance defaults and [Library embedding](#library-embedding) below. ## Configuration diff --git a/src/__tests__/cross-entry-point.test.ts b/src/__tests__/cross-entry-point.test.ts index 8dc1c38..f3d267e 100644 --- a/src/__tests__/cross-entry-point.test.ts +++ b/src/__tests__/cross-entry-point.test.ts @@ -1,6 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; +import { setupAllianceServer } from '../alliance/setup.js'; import { resolveAllianceConfig } from '../alliance/config.js'; +import { setPineconeClient } from '../core/index.js'; import { resolveConfig } from '../core/config.js'; +import { getDefaultServerContext } from '../core/server/server-context.js'; import * as guidedQueryTool from '../core/server/tools/guided-query-tool.js'; import { registerQueryTool } from '../core/server/tools/query-tool.js'; import { @@ -72,6 +75,32 @@ describe('cross-entry-point: core vs Alliance defaults', () => { expect(err.suggestion).toBe("Call suggest_query_params for namespace 'wg21' first"); }); + it('setupAllianceServer wires Alliance gate defaults: query returns FLOW_GATE', async () => { + const cfg = resolveAllianceConfig( + { apiKey: 'sk-test', indexName: 'my-index' }, + { PINECONE_API_KEY: 'sk-test', PINECONE_INDEX_NAME: 'my-index' } + ); + setPineconeClient( + new PineconeClient({ + apiKey: cfg.apiKey, + indexName: cfg.indexName, + defaultTopK: cfg.defaultTopK, + }) + ); + await setupAllianceServer({ config: cfg }); + const ctx = getDefaultServerContext(); + expect(ctx.getConfig().disableSuggestFlow).toBe(false); + + const server = createMockServer(); + registerQueryTool(server as never, ctx); + const raw = await server.getHandler('query')!({ + query_text: 'hello', + namespace: 'wg21', + preset: 'fast', + }); + assertToolErrorCode(raw, 'FLOW_GATE'); + }); + it('core setup registers guided_query handler', async () => { const cfg = resolveConfig({ apiKey: 'sk-cross', indexName: 'test-index' }); const ctx = createTestServerContext({ diff --git a/src/constants.test.ts b/src/constants.test.ts index 432c962..c499860 100644 --- a/src/constants.test.ts +++ b/src/constants.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + ALLIANCE_INSTRUCTIONS_APPENDIX, ALLIANCE_SERVER_INSTRUCTIONS, CORE_SERVER_INSTRUCTIONS, SERVER_INSTRUCTIONS, @@ -16,6 +17,14 @@ describe('server instructions', () => { expect(ALLIANCE_SERVER_INSTRUCTIONS).toMatch(/suggest_query_params/); }); + it('ALLIANCE_INSTRUCTIONS_APPENDIX does not duplicate core guided_query quickstart', () => { + expect(ALLIANCE_INSTRUCTIONS_APPENDIX).not.toMatch( + /Alliance quickstart: for most user questions, call `guided_query`/ + ); + expect(ALLIANCE_INSTRUCTIONS_APPENDIX).toMatch(/rag-hybrid/); + expect(ALLIANCE_INSTRUCTIONS_APPENDIX).toMatch(/suggest_query_params/); + }); + it('SERVER_INSTRUCTIONS aliases Alliance instructions', () => { expect(SERVER_INSTRUCTIONS).toBe(ALLIANCE_SERVER_INSTRUCTIONS); }); diff --git a/src/constants.ts b/src/constants.ts index 51ff196..5fad13a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -63,7 +63,7 @@ Usage: /** Alliance-only supplement appended to core instructions for {@link setupAllianceServer}. */ export const ALLIANCE_INSTRUCTIONS_APPENDIX = ` -Alliance quickstart: for most user questions, call \`guided_query\` with the user's question — it does namespace routing, suggestion, and execution in one shot and returns \`experimental.decision_trace\` you can show the user. The Alliance CLI and \`resolveAllianceConfig\` default the index to \`rag-hybrid\` (and rerank to \`bge-reranker-v2-m3\`) when those env vars are omitted. +Alliance deployment: The Alliance CLI and \`resolveAllianceConfig\` default the index to \`rag-hybrid\` (and rerank to \`bge-reranker-v2-m3\`) when those env vars are omitted. For manual flows with the full tool surface, call \`list_namespaces\` -> \`suggest_query_params\` -> \`query\` (use preset fast/detailed/full per suggestion) or \`count\` (the suggest step is a mandatory gate unless \`PINECONE_DISABLE_SUGGEST_FLOW=true\`).