Hybrid Search Router Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Let Qiongli coordinate provider-backed MCP literature search with Codex/Claude platform-native search without making MCP servers call platform search directly.
Architecture: Add a deterministic qiongli_search_plan MCP tool that returns provider queries, native-search queries, search execution mode, provenance labels, and agent instructions. The MCP tool plans native searches but does not execute Codex/Claude native search; the active agent executes platform-native search when available, then Qiongli workflows merge and log provider, native, and user-corpus evidence separately. Mirror the contract in the Python full MCP and the Node literature MCPB runtime so Codex, Claude Code, and Desktop/plugin bundles expose the same planning semantics.
Tech Stack: Python 3.12, unittest, Node.js ESM MCPB runtime, node:test, Qiongli workflow Markdown contracts, Codex/Claude local plugin artifacts.
File Structure
- Create
packages/python-qiongli/src/qiongli/bridges/hybrid_search_router.pyfor the shared Python search-plan contract and mode selection. - Modify
packages/python-qiongli/src/qiongli/bridges/literature_mcp_tools.pyto exposeqiongli_search_planin full Python MCP. - Modify
packages/python-qiongli/src/qiongli/bridges/mcp_tool_handlers.pyto routeqiongli_search_plan. - Create
tests/test_hybrid_search_router.pyfor pure mode-selection tests. - Modify
tests/test_mcp_tool_handlers.pyandtests/test_mcp_stdio_server.pyfor Python MCP tool exposure. - Create
packages/qiongli-literature-mcpb/server/search-plan.mjsfor the Node MCPB search-plan contract. - Modify
packages/qiongli-literature-mcpb/server/index.mjsandpackages/qiongli-literature-mcpb/manifest.jsonto exposeqiongli_search_plan. - Modify
packages/qiongli-literature-mcpb/test/tools.test.mjsandtests/test_literature_mcpb_artifact.pyfor Node MCPB parity. - Modify workflow source files:
content/workflow/SKILL.md,content/workflow/workflows/lit-review.md,content/workflow/workflows/paper-read.md, andcontent/skills/B_literature/academic-searcher.md. - Modify docs:
docs/advanced/cross-platform-mcp.md,docs/advanced/mcp-providers-setup.md,docs/guide/troubleshooting.md,docs/zh/advanced/mcp-providers-setup.md, anddocs/zh/guide/troubleshooting.md.
Task 1: Add Python Hybrid Search Plan Contract
Files:
Create:
packages/python-qiongli/src/qiongli/bridges/hybrid_search_router.pyCreate:
tests/test_hybrid_search_router.py[ ] Step 1: Write failing mode-selection tests
Create tests/test_hybrid_search_router.py:
from __future__ import annotations
import unittest
from bridges.hybrid_search_router import build_hybrid_search_plan
class HybridSearchRouterTests(unittest.TestCase):
def test_provider_and_native_search_yield_hybrid_mode(self) -> None:
plan = build_hybrid_search_plan(
{
"query": "AI adoption in accounting disclosure",
"platform": "codex",
"native_search_available": True,
"native_search_tools": ["codex_web_search"],
"fromYear": 2023,
"toYear": 2026,
"include_working_papers": True,
},
provider_capability_mode="provider_connected",
)
self.assertEqual(plan["search_execution_mode"], "hybrid_search")
self.assertEqual(plan["provider_capability_mode"], "provider_connected")
self.assertTrue(plan["native_search_available"])
self.assertIn("mcp:openalex", plan["provenance_labels"])
self.assertIn("mcp:semantic_scholar", plan["provenance_labels"])
self.assertIn("native:codex_web_search", plan["provenance_labels"])
self.assertGreaterEqual(len(plan["provider_queries"]), 1)
self.assertGreaterEqual(len(plan["native_search_queries"]), 1)
self.assertEqual(plan["execution_sequence"][0]["actor"], "agent")
self.assertEqual(plan["execution_sequence"][0]["action"], "call qiongli_literature_status")
self.assertIn("Do not treat native-search results as provider-reproducible records.", plan["agent_instructions"])
def test_provider_only_mode_when_native_search_unavailable(self) -> None:
plan = build_hybrid_search_plan(
{
"query": "audit committee expertise disclosure",
"platform": "cli",
"native_search_available": False,
},
provider_capability_mode="provider_connected",
)
self.assertEqual(plan["search_execution_mode"], "provider_connected")
self.assertFalse(plan["native_search_available"])
self.assertEqual(plan["native_search_queries"], [])
self.assertIn("Platform-native search was not declared available.", plan["limitations"])
def test_native_only_mode_when_provider_missing_but_native_available(self) -> None:
plan = build_hybrid_search_plan(
{
"query": "SSRN corporate disclosure generative AI",
"platform": "claude_code",
"native_search_available": True,
"native_search_tools": ["claude_web_search"],
"include_working_papers": True,
},
provider_capability_mode="strategy_only",
)
self.assertEqual(plan["search_execution_mode"], "native_only")
self.assertIn("native:claude_web_search", plan["provenance_labels"])
self.assertIn("Provider MCP search is unavailable; native results require explicit provenance labels.", plan["limitations"])
def test_strategy_only_when_no_provider_and_no_native_search(self) -> None:
plan = build_hybrid_search_plan(
{
"query": "earnings call disclosure",
"platform": "unknown",
"native_search_available": False,
},
provider_capability_mode="strategy_only",
)
self.assertEqual(plan["search_execution_mode"], "strategy_only")
self.assertEqual(plan["provider_queries"], [])
self.assertEqual(plan["native_search_queries"], [])
self.assertIn("No provider MCP search or platform-native search is available.", plan["limitations"])
def test_empty_query_returns_structured_strategy_gap(self) -> None:
plan = build_hybrid_search_plan(
{"query": " ", "platform": "codex", "native_search_available": True},
provider_capability_mode="provider_connected",
)
self.assertEqual(plan["search_execution_mode"], "strategy_only")
self.assertIn("Search query is empty.", plan["limitations"])
self.assertEqual(plan["provider_queries"], [])
self.assertEqual(plan["native_search_queries"], [])
if __name__ == "__main__":
unittest.main()- [ ] Step 2: Run test to verify red
Run:
python3 -m unittest tests.test_hybrid_search_router -vExpected: FAIL with ModuleNotFoundError: No module named 'bridges.hybrid_search_router'.
- [ ] Step 3: Implement the Python router
Create packages/python-qiongli/src/qiongli/bridges/hybrid_search_router.py:
from __future__ import annotations
from typing import Any
PROVIDER_LABELS = ("mcp:semantic_scholar", "mcp:openalex", "mcp:crossref", "mcp:pubmed", "mcp:arxiv")
DEFAULT_NATIVE_TOOLS = {
"codex": "codex_web_search",
"claude": "claude_web_search",
"claude_code": "claude_web_search",
"antigravity": "antigravity_search",
}
def build_hybrid_search_plan(
args: dict[str, Any],
*,
provider_capability_mode: str,
) -> dict[str, Any]:
query = str(args.get("query", "") or "").strip()
platform = _platform(args.get("platform"))
native_available = bool(args.get("native_search_available", False))
native_tools = _native_tools(args, platform, native_available)
limitations: list[str] = []
if not query:
return _empty_plan(
query=query,
platform=platform,
provider_capability_mode=provider_capability_mode,
native_available=native_available,
native_tools=native_tools,
limitations=["Search query is empty."],
)
provider_connected = provider_capability_mode == "provider_connected"
if provider_connected and native_available:
mode = "hybrid_search"
elif provider_connected:
mode = "provider_connected"
limitations.append("Platform-native search was not declared available.")
elif native_available:
mode = "native_only"
limitations.append("Provider MCP search is unavailable; native results require explicit provenance labels.")
else:
return _empty_plan(
query=query,
platform=platform,
provider_capability_mode=provider_capability_mode,
native_available=native_available,
native_tools=native_tools,
limitations=["No provider MCP search or platform-native search is available."],
)
provider_queries = _provider_queries(args, query) if provider_connected else []
native_queries = _native_queries(args, query) if native_available else []
provenance = []
if provider_queries:
provenance.extend(PROVIDER_LABELS)
provenance.extend(f"native:{tool}" for tool in native_tools)
provenance.append("user_corpus")
return {
"artifact_type": "qiongli_hybrid_search_plan",
"query": query,
"platform": platform,
"search_execution_mode": mode,
"provider_capability_mode": provider_capability_mode,
"native_search_available": native_available,
"native_search_tools": native_tools,
"provider_queries": provider_queries,
"native_search_queries": native_queries,
"provenance_labels": provenance,
"execution_sequence": _execution_sequence(mode),
"agent_instructions": _agent_instructions(),
"merge_policy": {
"dedupe_order": ["doi", "provider_id", "title_year_author"],
"preserve_source_rows": True,
"require_search_log": True,
},
"limitations": limitations,
}
def _empty_plan(
*,
query: str,
platform: str,
provider_capability_mode: str,
native_available: bool,
native_tools: list[str],
limitations: list[str],
) -> dict[str, Any]:
return {
"artifact_type": "qiongli_hybrid_search_plan",
"query": query,
"platform": platform,
"search_execution_mode": "strategy_only",
"provider_capability_mode": provider_capability_mode,
"native_search_available": native_available,
"native_search_tools": native_tools,
"provider_queries": [],
"native_search_queries": [],
"provenance_labels": ["user_corpus"],
"execution_sequence": _execution_sequence("strategy_only"),
"agent_instructions": _agent_instructions(),
"merge_policy": {
"dedupe_order": ["doi", "provider_id", "title_year_author"],
"preserve_source_rows": True,
"require_search_log": True,
},
"limitations": limitations,
}
def _provider_queries(args: dict[str, Any], query: str) -> list[dict[str, Any]]:
return [
{
"query_id": "P1",
"query": query,
"search_mode": str(args.get("search_mode", args.get("searchMode", "review")) or "review"),
"fromYear": args.get("fromYear"),
"toYear": args.get("toYear"),
"venue_filter": args.get("venue_filter", args.get("venueFilter", "")),
"document_types": args.get("document_types", args.get("documentTypes", [])),
}
]
def _native_queries(args: dict[str, Any], query: str) -> list[dict[str, Any]]:
queries = [
{
"query_id": "N1",
"query": query,
"purpose": "current web/PDF/working-paper supplement",
}
]
if bool(args.get("include_working_papers", args.get("includeWorkingPapers", False))):
queries.append(
{
"query_id": "N2",
"query": f'{query} SSRN OR NBER OR CEPR OR working paper filetype:pdf',
"purpose": "working paper and author PDF supplement",
}
)
return queries
def _native_tools(args: dict[str, Any], platform: str, native_available: bool) -> list[str]:
raw = args.get("native_search_tools", args.get("nativeSearchTools", []))
if isinstance(raw, list):
tools = [str(item).strip() for item in raw if str(item).strip()]
else:
tools = []
if not tools and native_available:
tools = [DEFAULT_NATIVE_TOOLS.get(platform, "platform_native_search")]
return tools
def _execution_sequence(mode: str) -> list[dict[str, str]]:
sequence = [
{"actor": "agent", "action": "call qiongli_literature_status"},
{"actor": "agent", "action": "call qiongli_search_plan"},
]
if mode in {"provider_connected", "hybrid_search"}:
sequence.append({"actor": "agent", "action": "call qiongli_literature_search for provider_queries"})
if mode in {"native_only", "hybrid_search"}:
sequence.append({"actor": "agent", "action": "execute platform-native search for native_search_queries"})
sequence.append({"actor": "agent", "action": "merge, dedupe, and write provenance-labelled search_log.md"})
return sequence
def _agent_instructions() -> list[str]:
return [
"MCP servers must not call Codex or Claude native search directly.",
"The active agent executes native_search_queries only when the platform exposes native search.",
"Do not treat native-search results as provider-reproducible records.",
"Write provider, native, and user-corpus records with distinct provenance labels.",
]
def _platform(raw: Any) -> str:
value = str(raw or "unknown").strip().lower().replace("-", "_")
return value if value in {"codex", "claude", "claude_code", "antigravity", "cli"} else "unknown"- [ ] Step 4: Run pure router tests
Run:
python3 -m unittest tests.test_hybrid_search_router -vExpected: PASS.
Task 2: Expose qiongli_search_plan In Python MCP
Files:
Modify:
packages/python-qiongli/src/qiongli/bridges/literature_mcp_tools.pyModify:
packages/python-qiongli/src/qiongli/bridges/mcp_tool_handlers.pyModify:
tests/test_mcp_tool_handlers.pyModify:
tests/test_mcp_stdio_server.py[ ] Step 1: Write failing MCP tool handler tests
In tests/test_mcp_tool_handlers.py, update test_tool_definitions_include_config_and_evidence_tools expected names to include:
"qiongli_search_plan",Add this test:
def test_search_plan_tool_returns_hybrid_plan_without_running_native_search(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
with mock.patch.dict(
"os.environ",
{"QIONGLI_CONFIG_HOME": str(root / "config")},
clear=False,
):
set_provider_value("openalex", "api-key", "openalex-secret-key")
result = call_qiongli_tool(
"qiongli_search_plan",
{
"query": "AI disclosure accounting working papers",
"platform": "codex",
"native_search_available": True,
"native_search_tools": ["codex_web_search"],
"include_working_papers": True,
},
)
payload = result["structuredContent"]
rendered = json.dumps(result, sort_keys=True)
self.assertFalse(result["isError"])
self.assertEqual(payload["search_execution_mode"], "hybrid_search")
self.assertEqual(payload["provider_capability_mode"], "provider_connected")
self.assertIn("native:codex_web_search", payload["provenance_labels"])
self.assertIn("call qiongli_literature_search", json.dumps(payload["execution_sequence"]))
self.assertIn("execute platform-native search", json.dumps(payload["execution_sequence"]))
self.assertNotIn("openalex-secret-key", rendered)In tests/test_mcp_stdio_server.py, add:
self.assertIn("qiongli_search_plan", tool_names)- [ ] Step 2: Run tests to verify red
Run:
python3 -m unittest tests.test_mcp_tool_handlers tests.test_mcp_stdio_server -vExpected: FAIL because qiongli_search_plan is not defined or routed.
- [ ] Step 3: Add Python MCP tool definition and handler
In packages/python-qiongli/src/qiongli/bridges/literature_mcp_tools.py, import the router:
from bridges.hybrid_search_router import build_hybrid_search_planAdd this tool declaration immediately after qiongli_literature_status:
{
"name": "qiongli_search_plan",
"description": (
"Plan hybrid Qiongli literature search across MCP providers and platform-native search. "
"This tool returns native-search instructions but does not execute Codex or Claude native search."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"platform": {
"type": "string",
"enum": ["codex", "claude", "claude_code", "antigravity", "cli", "unknown"],
"default": "unknown",
},
"native_search_available": {"type": "boolean", "default": False},
"native_search_tools": {"type": "array", "items": {"type": "string"}},
"include_working_papers": {"type": "boolean", "default": False},
"fromYear": {"type": ["integer", "string"]},
"toYear": {"type": ["integer", "string"]},
"search_mode": {
"type": "string",
"enum": ["auto", "topic", "title", "doi", "review", "systematic_review"],
},
"venue_filter": {"type": "string"},
"document_types": {"type": "array", "items": {"type": "string"}},
},
"additionalProperties": True,
},
},Add:
def handle_search_plan(args: dict[str, Any]) -> dict[str, Any]:
status = handle_literature_status(args)
return build_hybrid_search_plan(
args,
provider_capability_mode=str(status.get("capability_mode") or "strategy_only"),
)In packages/python-qiongli/src/qiongli/bridges/mcp_tool_handlers.py, import and route:
handle_search_plan,Add to handlers:
"qiongli_search_plan": handle_search_plan,- [ ] Step 4: Run MCP Python tests
Run:
python3 -m unittest tests.test_hybrid_search_router tests.test_mcp_tool_handlers tests.test_mcp_stdio_server -vExpected: PASS.
Task 3: Mirror qiongli_search_plan In Node Literature MCPB
Files:
Create:
packages/qiongli-literature-mcpb/server/search-plan.mjsModify:
packages/qiongli-literature-mcpb/server/index.mjsModify:
packages/qiongli-literature-mcpb/manifest.jsonModify:
packages/qiongli-literature-mcpb/test/tools.test.mjsModify:
tests/test_literature_mcpb_artifact.py[ ] Step 1: Write failing Node MCPB tests
In packages/qiongli-literature-mcpb/test/tools.test.mjs, update the tool declaration name list to include "qiongli_search_plan" immediately after "qiongli_literature_status".
Add this import:
handleSearchPlan,Add these tests:
test("search plan returns hybrid mode without executing native search", () => {
const plan = handleSearchPlan({
query: "AI disclosure accounting working papers",
platform: "codex",
native_search_available: true,
native_search_tools: ["codex_web_search"],
include_working_papers: true
}, {
env: {
QIONGLI_MCPB_OPENALEX_API_KEY: "openalex-secret"
}
});
const rendered = JSON.stringify(plan);
assert.equal(plan.search_execution_mode, "hybrid_search");
assert.equal(plan.provider_capability_mode, "provider_connected");
assert.ok(plan.provenance_labels.includes("native:codex_web_search"));
assert.ok(JSON.stringify(plan.execution_sequence).includes("execute platform-native search"));
assert.ok(!rendered.includes("openalex-secret"));
});
test("search plan returns native-only mode when providers are missing but native search is available", () => {
const plan = handleSearchPlan({
query: "SSRN generative AI disclosure",
platform: "claude_code",
native_search_available: true,
native_search_tools: ["claude_web_search"]
}, {
env: {}
});
assert.equal(plan.search_execution_mode, "native_only");
assert.equal(plan.provider_capability_mode, "strategy_only");
assert.ok(plan.limitations.includes("Provider MCP search is unavailable; native results require explicit provenance labels."));
});In tests/test_literature_mcpb_artifact.py, add "qiongli_search_plan" to test_literature_mcpb_manifest_declares_expected_tools.
- [ ] Step 2: Run Node and artifact tests to verify red
Run:
npm --prefix packages/qiongli-literature-mcpb test
python3 -m unittest tests.test_literature_mcpb_artifact -vExpected: FAIL because Node MCPB has no qiongli_search_plan declaration, handler, or manifest entry.
- [ ] Step 3: Implement Node search-plan helper
Create packages/qiongli-literature-mcpb/server/search-plan.mjs:
const PROVIDER_LABELS = ["mcp:semantic_scholar", "mcp:openalex", "mcp:crossref", "mcp:pubmed", "mcp:arxiv"];
const DEFAULT_NATIVE_TOOLS = {
codex: "codex_web_search",
claude: "claude_web_search",
claude_code: "claude_web_search",
antigravity: "antigravity_search"
};
export function buildHybridSearchPlan(input = {}, providerCapabilityMode = "strategy_only") {
const query = String(input.query || "").trim();
const platform = normalizePlatform(input.platform);
const nativeSearchAvailable = Boolean(input.native_search_available || input.nativeSearchAvailable);
const nativeSearchTools = nativeTools(input, platform, nativeSearchAvailable);
if (!query) {
return emptyPlan({
query,
platform,
providerCapabilityMode,
nativeSearchAvailable,
nativeSearchTools,
limitations: ["Search query is empty."]
});
}
const providerConnected = providerCapabilityMode === "provider_connected";
let searchExecutionMode = "strategy_only";
const limitations = [];
if (providerConnected && nativeSearchAvailable) {
searchExecutionMode = "hybrid_search";
} else if (providerConnected) {
searchExecutionMode = "provider_connected";
limitations.push("Platform-native search was not declared available.");
} else if (nativeSearchAvailable) {
searchExecutionMode = "native_only";
limitations.push("Provider MCP search is unavailable; native results require explicit provenance labels.");
} else {
return emptyPlan({
query,
platform,
providerCapabilityMode,
nativeSearchAvailable,
nativeSearchTools,
limitations: ["No provider MCP search or platform-native search is available."]
});
}
const providerQueries = providerConnected ? providerQueriesFor(input, query) : [];
const nativeSearchQueries = nativeSearchAvailable ? nativeQueriesFor(input, query) : [];
const provenanceLabels = [];
if (providerQueries.length > 0) {
provenanceLabels.push(...PROVIDER_LABELS);
}
provenanceLabels.push(...nativeSearchTools.map((tool) => `native:${tool}`));
provenanceLabels.push("user_corpus");
return {
artifact_type: "qiongli_hybrid_search_plan",
query,
platform,
search_execution_mode: searchExecutionMode,
provider_capability_mode: providerCapabilityMode,
native_search_available: nativeSearchAvailable,
native_search_tools: nativeSearchTools,
provider_queries: providerQueries,
native_search_queries: nativeSearchQueries,
provenance_labels: provenanceLabels,
execution_sequence: executionSequence(searchExecutionMode),
agent_instructions: agentInstructions(),
merge_policy: mergePolicy(),
limitations
};
}
function emptyPlan({ query, platform, providerCapabilityMode, nativeSearchAvailable, nativeSearchTools, limitations }) {
return {
artifact_type: "qiongli_hybrid_search_plan",
query,
platform,
search_execution_mode: "strategy_only",
provider_capability_mode: providerCapabilityMode,
native_search_available: nativeSearchAvailable,
native_search_tools: nativeSearchTools,
provider_queries: [],
native_search_queries: [],
provenance_labels: ["user_corpus"],
execution_sequence: executionSequence("strategy_only"),
agent_instructions: agentInstructions(),
merge_policy: mergePolicy(),
limitations
};
}
function providerQueriesFor(input, query) {
return [{
query_id: "P1",
query,
search_mode: String(input.search_mode || input.searchMode || "review"),
fromYear: input.fromYear,
toYear: input.toYear,
venue_filter: input.venue_filter || input.venueFilter || "",
document_types: input.document_types || input.documentTypes || []
}];
}
function nativeQueriesFor(input, query) {
const queries = [{
query_id: "N1",
query,
purpose: "current web/PDF/working-paper supplement"
}];
if (Boolean(input.include_working_papers || input.includeWorkingPapers)) {
queries.push({
query_id: "N2",
query: `${query} SSRN OR NBER OR CEPR OR working paper filetype:pdf`,
purpose: "working paper and author PDF supplement"
});
}
return queries;
}
function nativeTools(input, platform, nativeSearchAvailable) {
const raw = input.native_search_tools || input.nativeSearchTools || [];
const tools = Array.isArray(raw) ? raw.map((item) => String(item).trim()).filter(Boolean) : [];
if (tools.length === 0 && nativeSearchAvailable) {
return [DEFAULT_NATIVE_TOOLS[platform] || "platform_native_search"];
}
return tools;
}
function executionSequence(mode) {
const sequence = [
{ actor: "agent", action: "call qiongli_literature_status" },
{ actor: "agent", action: "call qiongli_search_plan" }
];
if (["provider_connected", "hybrid_search"].includes(mode)) {
sequence.push({ actor: "agent", action: "call qiongli_literature_search for provider_queries" });
}
if (["native_only", "hybrid_search"].includes(mode)) {
sequence.push({ actor: "agent", action: "execute platform-native search for native_search_queries" });
}
sequence.push({ actor: "agent", action: "merge, dedupe, and write provenance-labelled search_log.md" });
return sequence;
}
function agentInstructions() {
return [
"MCP servers must not call Codex or Claude native search directly.",
"The active agent executes native_search_queries only when the platform exposes native search.",
"Do not treat native-search results as provider-reproducible records.",
"Write provider, native, and user-corpus records with distinct provenance labels."
];
}
function mergePolicy() {
return {
dedupe_order: ["doi", "provider_id", "title_year_author"],
preserve_source_rows: true,
require_search_log: true
};
}
function normalizePlatform(raw) {
const value = String(raw || "unknown").trim().toLowerCase().replace(/-/g, "_");
return ["codex", "claude", "claude_code", "antigravity", "cli"].includes(value) ? value : "unknown";
}- [ ] Step 4: Wire Node MCPB declarations and handler
In packages/qiongli-literature-mcpb/server/index.mjs, import:
import { buildHybridSearchPlan } from "./search-plan.mjs";Add a TOOL_DECLARATIONS entry immediately after qiongli_literature_status:
{
name: "qiongli_search_plan",
description: "Plan hybrid Qiongli literature search across MCP providers and platform-native search. This tool returns native-search instructions but does not execute Codex or Claude native search.",
inputSchema: {
type: "object",
additionalProperties: true,
properties: {
query: { type: "string" },
platform: {
type: "string",
enum: ["codex", "claude", "claude_code", "antigravity", "cli", "unknown"],
default: "unknown"
},
native_search_available: { type: "boolean", default: false },
native_search_tools: { type: "array", items: { type: "string" } },
include_working_papers: { type: "boolean", default: false },
fromYear: { type: ["integer", "string"] },
toYear: { type: ["integer", "string"] },
search_mode: {
type: "string",
enum: ["auto", "topic", "title", "doi", "review", "systematic_review"]
},
venue_filter: { type: "string" },
document_types: { type: "array", items: { type: "string" } }
}
}
},Export:
export function handleSearchPlan(input = {}, context = {}) {
const status = handleStatus(context);
return buildHybridSearchPlan(input, status.capability_mode || "strategy_only");
}In handleToolCall, add before qiongli_literature_search:
if (name === "qiongli_search_plan") {
return toolResult(handleSearchPlan(input, context));
}In packages/qiongli-literature-mcpb/manifest.json, add a matching qiongli_search_plan tool entry after qiongli_literature_status.
- [ ] Step 5: Run Node MCPB parity tests
Run:
npm --prefix packages/qiongli-literature-mcpb test
python3 -m unittest tests.test_literature_mcpb_artifact tests.test_plugin_distribution_contract tests.test_plugin_artifacts -vExpected: PASS. Plugin artifact tests should pass because tooling/scripts/build_plugin_artifacts.py copies packages/qiongli-literature-mcpb/server/ into plugin payloads.
Task 4: Update Workflow Contracts For Hybrid Search Execution
Files:
Modify:
content/workflow/SKILL.mdModify:
content/workflow/workflows/lit-review.mdModify:
content/workflow/workflows/paper-read.mdModify:
content/skills/B_literature/academic-searcher.mdModify:
tests/test_literature_contract.pyModify:
tests/test_mcp_provider_docs.pyModify if generated text requires it:
packages/python-qiongli/src/qiongli/subject_materializer.py[ ] Step 1: Write failing workflow contract tests
In tests/test_literature_contract.py, add:
def test_literature_workflows_document_hybrid_search_router(self) -> None:
workflow_paths = (
LAYOUT.workflow / "SKILL.md",
LAYOUT.workflow / "workflows" / "lit-review.md",
LAYOUT.workflow / "workflows" / "paper-read.md",
LAYOUT.skills / "B_literature" / "academic-searcher.md",
)
required_tokens = (
"qiongli_search_plan",
"hybrid_search",
"native_only",
"provider_connected",
"strategy_only",
"provenance labels",
"MCP servers must not call Codex or Claude native search directly",
)
for path in workflow_paths:
with self.subTest(path=str(path.relative_to(REPO_ROOT))):
content = path.read_text(encoding="utf-8")
for token in required_tokens:
self.assertIn(token, content)In tests/test_mcp_provider_docs.py, add:
def test_docs_explain_hybrid_search_router_boundary(self) -> None:
content = (REPO_ROOT / "content" / "workflow" / "SKILL.md").read_text(encoding="utf-8")
self.assertIn("qiongli_search_plan", content)
self.assertIn("hybrid_search", content)
self.assertIn("native_only", content)
self.assertIn("MCP servers must not call Codex or Claude native search directly", content)- [ ] Step 2: Run tests to verify red
Run:
python3 -m unittest tests.test_literature_contract tests.test_mcp_provider_docs -vExpected: FAIL because the workflow docs do not yet define qiongli_search_plan, hybrid_search, or native_only.
- [ ] Step 3: Update
content/workflow/SKILL.md
Under ## Literature Provider Configuration, add:
- For literature search, literature review, paper reading metadata lookup, gap finding, citation snowballing, and evidence synthesis, use `qiongli_search_plan` after `qiongli_literature_status` when the tool is visible. The plan selects `search_execution_mode`: `hybrid_search` when provider MCP and platform-native search are both available; `provider_connected` when only provider MCP is available; `native_only` when provider MCP is unavailable but Codex/Claude platform-native search is available; and `strategy_only` only when neither provider MCP nor platform-native search nor user corpus is available.
- MCP servers must not call Codex or Claude native search directly. The active agent executes `native_search_queries` returned by `qiongli_search_plan` with the platform-native search capability when available, then merges those results with provider records.
- Preserve provenance labels in all search artifacts: `mcp:openalex`, `mcp:semantic_scholar`, `mcp:crossref`, `mcp:pubmed`, `mcp:arxiv`, `native:codex_web_search`, `native:claude_web_search`, and `user_corpus`. Native-search results are supplemental unless the workflow explicitly marks the run as `native_only`; do not present them as provider-reproducible records.- [ ] Step 4: Update
lit-review.mdexecution phases
In content/workflow/workflows/lit-review.md, replace the Phase 2 item that records only provider_connected / strategy_only with:
4. Call `qiongli_literature_status` when visible, then call `qiongli_search_plan` with:
- `query`
- `platform`
- `native_search_available`
- `native_search_tools`
- date range and document filters
5. Record `search_execution_mode` as one of `hybrid_search`, `provider_connected`, `native_only`, or `strategy_only`.
6. Record provider capability mode separately as `provider_connected` or `strategy_only`.In Phase 3, replace the supplemental native-search item with:
4. If `search_execution_mode` is `hybrid_search` or `native_only`, execute the `native_search_queries` returned by `qiongli_search_plan` with the active platform's native search capability. MCP servers must not call Codex or Claude native search directly.
5. Log provider and native searches in `search_log.md` with distinct provenance labels, then normalize and dedupe records without merging away source provenance.- [ ] Step 5: Update
paper-read.mdretrieval rules
In content/workflow/workflows/paper-read.md, replace the capability-mode paragraph in Step 1 with:
Record `search_execution_mode` from `qiongli_search_plan` as `hybrid_search`, `provider_connected`, `native_only`, or `strategy_only`, and record provider capability mode separately as `provider_connected` or `strategy_only`. If no MCP/provider, platform-native search, or user-supplied metadata is available, use user-supplied metadata only and keep that evidence boundary visible. MCP servers must not call Codex or Claude native search directly; the active agent executes platform-native lookups when available.- [ ] Step 6: Update
academic-searcher.mdownership boundary
In content/skills/B_literature/academic-searcher.md, add after the provider ownership table:
Hybrid search coordination belongs to the Qiongli workflow/router layer. Use `qiongli_search_plan` to decide whether a run is `hybrid_search`, `provider_connected`, `native_only`, or `strategy_only`. The MCP/provider layer owns provider calls and raw provider hit capture; the active agent owns any platform-native search calls; this skill owns logging, normalization, deduplication, and diagnostics. MCP servers must not call Codex or Claude native search directly.- [ ] Step 7: Run workflow contract tests
Run:
python3 -m unittest tests.test_literature_contract tests.test_mcp_provider_docs tests.test_subject_materializer -vExpected: PASS. If subject materialization tests fail because hardcoded provider guidance in subject_materializer.py is missing the new text, add the same concise hybrid-search bullets to the materializer's injected fallback text.
Task 5: Update User-Facing Docs
Files:
Modify:
docs/advanced/cross-platform-mcp.mdModify:
docs/advanced/mcp-providers-setup.mdModify:
docs/guide/troubleshooting.mdModify:
docs/zh/advanced/mcp-providers-setup.mdModify:
docs/zh/guide/troubleshooting.mdModify:
tests/test_mcp_provider_docs.pyModify:
tests/test_cli_setup_docs.py[ ] Step 1: Write failing docs tests
In tests/test_mcp_provider_docs.py, add:
def test_provider_docs_explain_hybrid_search_modes(self) -> None:
docs = {
"docs/advanced/cross-platform-mcp.md": (REPO_ROOT / "docs" / "advanced" / "cross-platform-mcp.md").read_text(encoding="utf-8"),
"docs/advanced/mcp-providers-setup.md": (REPO_ROOT / "docs" / "advanced" / "mcp-providers-setup.md").read_text(encoding="utf-8"),
"docs/zh/advanced/mcp-providers-setup.md": (REPO_ROOT / "docs" / "zh" / "advanced" / "mcp-providers-setup.md").read_text(encoding="utf-8"),
}
for label, content in docs.items():
with self.subTest(label=label):
self.assertIn("qiongli_search_plan", content)
self.assertIn("hybrid_search", content)
self.assertIn("native_only", content)
self.assertIn("MCP servers", content)
self.assertIn("native search", content)In tests/test_cli_setup_docs.py, extend the troubleshooting tests to assert:
self.assertIn("hybrid_search", troubleshooting)
self.assertIn("native_only", troubleshooting)and in the Chinese troubleshooting test:
self.assertIn("hybrid_search", troubleshooting)
self.assertIn("native_only", troubleshooting)- [ ] Step 2: Run docs tests to verify red
Run:
python3 -m unittest tests.test_mcp_provider_docs tests.test_cli_setup_docs -vExpected: FAIL because the docs do not yet describe qiongli_search_plan and hybrid modes.
- [ ] Step 3: Update English docs
In docs/advanced/cross-platform-mcp.md, add to the literature tools list:
- `qiongli_search_plan`: returns provider queries, platform-native search queries, `search_execution_mode`, provenance labels, and merge instructions. It never executes Codex or Claude native search itself.In docs/advanced/mcp-providers-setup.md, add a "Hybrid Search Router" section:
## Hybrid Search Router
Use `qiongli_search_plan` after `qiongli_literature_status` for literature review, paper lookup, gap finding, citation snowballing, and evidence synthesis. The tool returns one of four execution modes:
| Mode | Meaning |
| --- | --- |
| `hybrid_search` | Provider MCP and platform-native search are both available. |
| `provider_connected` | Provider MCP is available; platform-native search was not declared available. |
| `native_only` | Provider MCP is unavailable, but Codex/Claude platform-native search is available. |
| `strategy_only` | Neither provider MCP nor platform-native search nor user corpus is available. |
MCP servers must not call Codex or Claude native search directly. The active agent executes native search queries returned by `qiongli_search_plan`, then logs them with provenance labels such as `native:codex_web_search` or `native:claude_web_search`. Provider records keep labels such as `mcp:openalex` and `mcp:semantic_scholar`.In docs/guide/troubleshooting.md, update the strategy_only entry with:
- If platform-native search is available, ask Qiongli to call `qiongli_search_plan` and use `native_only` or `hybrid_search` rather than `strategy_only`.- [ ] Step 4: Update Chinese docs
In docs/zh/advanced/mcp-providers-setup.md, add:
## Hybrid Search Router
文献综述、单篇论文查找、gap finding、citation snowballing 和 evidence synthesis 应在 `qiongli_literature_status` 之后调用 `qiongli_search_plan`。该工具返回四种执行模式:
| Mode | 含义 |
| --- | --- |
| `hybrid_search` | Provider MCP 和平台原生搜索都可用。 |
| `provider_connected` | Provider MCP 可用,但当前未声明平台原生搜索可用。 |
| `native_only` | Provider MCP 不可用,但 Codex/Claude 平台原生搜索可用。 |
| `strategy_only` | Provider MCP、平台原生搜索和用户语料都不可用。 |
MCP servers 不能直接调用 Codex 或 Claude 的 native search。当前 agent 负责执行 `qiongli_search_plan` 返回的 native search queries,然后用 `native:codex_web_search` 或 `native:claude_web_search` 等 provenance labels 记录。Provider records 保留 `mcp:openalex`、`mcp:semantic_scholar` 等 labels。In docs/zh/guide/troubleshooting.md, update the strategy_only entry with:
- 如果当前平台有原生搜索能力,要求 Qiongli 调用 `qiongli_search_plan`,并使用 `native_only` 或 `hybrid_search`,不要直接降级为 `strategy_only`。- [ ] Step 5: Run docs tests
Run:
python3 -m unittest tests.test_mcp_provider_docs tests.test_cli_setup_docs -vExpected: PASS.
Task 6: Verify Packaging, Release Artifacts, And Boundaries
Files:
Read/verify all modified files.
[ ] Step 1: Run focused Python and Node tests
Run:
python3 -m unittest \
tests.test_hybrid_search_router \
tests.test_mcp_tool_handlers \
tests.test_mcp_stdio_server \
tests.test_literature_contract \
tests.test_mcp_provider_docs \
tests.test_cli_setup_docs \
tests.test_literature_mcpb_artifact \
tests.test_plugin_distribution_contract \
tests.test_plugin_artifacts \
tests.test_subject_materializer \
-v
npm --prefix packages/qiongli-literature-mcpb testExpected: PASS.
- [ ] Step 2: Run syntax and whitespace checks
Run:
python3 -m py_compile \
packages/python-qiongli/src/qiongli/bridges/hybrid_search_router.py \
packages/python-qiongli/src/qiongli/bridges/literature_mcp_tools.py \
packages/python-qiongli/src/qiongli/bridges/mcp_tool_handlers.py
node --check packages/qiongli-literature-mcpb/server/search-plan.mjs
node --check packages/qiongli-literature-mcpb/server/index.mjs
git diff --checkExpected: all commands exit 0.
- [ ] Step 3: Build MCPB and plugin artifact checks
Run:
python3 scripts/build_literature_mcpb.py --dist-dir /tmp/qiongli-mcpb-check
python3 -m unittest tests.test_literature_mcpb_artifact tests.test_plugin_distribution_contract tests.test_plugin_artifacts -vExpected: the MCPB artifact includes server/search-plan.mjs, and materialized Codex/Claude plugin payloads include the updated mcp/qiongli-literature-provider/search-plan.mjs copied from the MCPB server directory.
- [ ] Step 4: Repository boundary review
Run:
git status --short --untracked-files=all
git diff --name-only
rg -n "sk-[A-Za-z0-9]|api[_-]?key\\s*[:=]\\s*['\\\"][^'\\\"]+|/Users/pengjiaxin|BEGIN RSA|BEGIN OPENSSH|password\\s*[:=]" \
packages/python-qiongli/src/qiongli/bridges \
packages/qiongli-literature-mcpb \
content/workflow \
content/skills/B_literature \
docs/advanced \
docs/guide \
docs/zh/advanced \
docs/zh/guide \
testsExpected findings:
No provider secrets or real API keys are introduced.
No machine-specific absolute paths are committed.
No generated plugin payload directories are edited directly; plugin payload updates come from
packages/qiongli-literature-mcpb/server/via the existing artifact builder.The new
qiongli_search_plancontract is implemented in both Python full MCP and Node MCPB.The tool returns native-search instructions but does not execute platform-native search.
[ ] Step 5: Commit after existing work is separated
Because the current working tree already contains wrapper-skill and Codex MCP UX changes, inspect git status --short --untracked-files=all before staging. Commit this hybrid router work separately from earlier changes:
git add \
packages/python-qiongli/src/qiongli/bridges/hybrid_search_router.py \
packages/python-qiongli/src/qiongli/bridges/literature_mcp_tools.py \
packages/python-qiongli/src/qiongli/bridges/mcp_tool_handlers.py \
packages/qiongli-literature-mcpb/server/search-plan.mjs \
packages/qiongli-literature-mcpb/server/index.mjs \
packages/qiongli-literature-mcpb/manifest.json \
content/workflow/SKILL.md \
content/workflow/workflows/lit-review.md \
content/workflow/workflows/paper-read.md \
content/skills/B_literature/academic-searcher.md \
docs/advanced/cross-platform-mcp.md \
docs/advanced/mcp-providers-setup.md \
docs/guide/troubleshooting.md \
docs/zh/advanced/mcp-providers-setup.md \
docs/zh/guide/troubleshooting.md \
tests/test_hybrid_search_router.py \
tests/test_mcp_tool_handlers.py \
tests/test_mcp_stdio_server.py \
tests/test_literature_contract.py \
tests/test_mcp_provider_docs.py \
tests/test_cli_setup_docs.py \
tests/test_literature_mcpb_artifact.py \
packages/qiongli-literature-mcpb/test/tools.test.mjs
git commit -m "feat(search): add hybrid literature search planning"If content/workflow/SKILL.md, docs/guide/troubleshooting.md, or shared tests also contain previous Codex MCP UX edits, stage hunks carefully so each commit remains reviewable.
Self-Review
Spec coverage: The plan keeps MCP provider search and Codex/Claude native search collaborative without changing MCP call direction. qiongli_search_plan plans native searches, agents execute them, and workflows merge results with provenance labels.
Placeholder scan: The plan contains concrete file paths, commands, expected outcomes, and code snippets. It avoids open-ended placeholders and keeps implementation steps directly executable.
Type consistency: The canonical field is search_execution_mode, with values hybrid_search, provider_connected, native_only, and strategy_only. Provider status remains provider_capability_mode, preserving the existing provider_connected / strategy_only provider capability contract.