Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ Parameter names follow the CLI flags except for the established MCP names
(`--include-evals`). Terminal-only flags (`--json`,
`--implementation-coverage`, `--flat`) have no parameter because MCP output is
always structured. Hosted differences from the CLI: `limit` is capped at 25,
`search_papers` defaults to `keyword` mode, `get_paper_info` returns compact
official-first resources, and `read_paper` is chunked.
`get_paper_info` returns compact official-first resources, and `read_paper`
is chunked. `search_papers` defaults to `hybrid` mode like `pwc search`.
`tests/test_parity.py` fails when the CLI parser and the tool schemas drift.

All tools are annotated read-only and idempotent. Search is deterministic;
the caller controls keyword, hybrid, or semantic mode. `read_paper` fetches at
most one 64 KiB catalog chunk per call and returns a signed, one-hour
the caller controls hybrid (default), keyword, or semantic mode. `read_paper`
fetches at most one 64 KiB catalog chunk per call and returns a signed, one-hour
continuation cursor when more Markdown remains. Continuations stay pinned to
the resolved paper and content version, so a changed paper fails with an
explicit restart response.
Expand Down
10 changes: 6 additions & 4 deletions mcp_server/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Papers With Code MCP tools for searching and reading AI/ML papers,
compatibility: "Requires an MCP client connected to https://paperswithcode.co/mcp with the Papers With Code tools available."
---

Generated for `pwc-mcp v0.2.2` and stock-client MCP protocol `2025-11-25`.
Generated for `pwc-mcp v0.2.3` and stock-client MCP protocol `2025-11-25`.

The tools query the public [Papers With Code](https://paperswithcode.co) catalog
anonymously and are read-only. Every tool runs the matching `pwc` CLI research
Expand Down Expand Up @@ -108,9 +108,11 @@ available and the user needs one of those capabilities.

- Tool results use stable, versioned structured output with compact text
fallbacks. Prefer structured fields over parsing the text fallback.
- Search mode is deterministic: `keyword` is the default; `hybrid` adds dense
retrieval and `semantic` uses it alone. Both `hybrid` and `semantic` count
toward the semantic search rate limit.
- Search mode is deterministic: `hybrid` is the default, matching
`pwc search`; `keyword` uses lexical retrieval alone and `semantic` uses
dense retrieval alone. Both `hybrid` and `semantic` count toward the semantic
search rate limit, so pass `mode: "keyword"` for exact terminology or when
that limit is close.
- Catalog-filtered paper lists fail closed unless the server confirms every
requested filter; never treat results from an older server as filtered.
Parameter-filtered leaderboards fail closed unless the server confirms
Expand Down
4 changes: 2 additions & 2 deletions mcp_server/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ Tools run the CLI handlers in-process through the shared cached transport, so
validation, fail-closed filter confirmation, and the JSON payload are the
CLI's. Every result includes that payload as `data` beside typed projections.
Terminal-only flags have no parameter. The hosted service caps `limit` at 25,
defaults `search_papers` to keyword mode, returns compact official-first paper
resources, and serves `read_paper` in chunks.
returns compact official-first paper resources, and serves `read_paper` in
chunks. `search_papers` defaults to hybrid mode, the same as `pwc search`.

Expose these resource templates:

Expand Down
2 changes: 1 addition & 1 deletion mcp_server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pwc-mcp"
version = "0.2.2"
version = "0.2.3"
description = "Read-only Papers With Code MCP server"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion mcp_server/src/pwc_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Read-only Papers With Code MCP server."""

__version__ = "0.2.2"
__version__ = "0.2.3"
6 changes: 3 additions & 3 deletions mcp_server/src/pwc_mcp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from pwc_mcp import __version__
from pwc_mcp.catalog import CatalogClient
from pwc_mcp.server import TOOL_COMMANDS, Catalog, build_server
from pwc_mcp.server import DEFAULT_SEARCH_MODE, TOOL_COMMANDS, Catalog, build_server

LOGGER = logging.getLogger("pwc_mcp.requests")
# MCP SDK diagnostics can include peer-supplied tool names and resource URIs.
Expand Down Expand Up @@ -270,7 +270,7 @@ async def replay() -> Message:
def _tool_and_semantic(headers: Headers, body: bytes) -> tuple[str | None, bool]:
header_tool = headers.get("mcp-name")
body_tool = None
mode = None
mode = DEFAULT_SEARCH_MODE
try:
payload = json.loads(body)
except (UnicodeDecodeError, json.JSONDecodeError):
Expand All @@ -286,7 +286,7 @@ def _tool_and_semantic(headers: Headers, body: bytes) -> tuple[str | None, bool]
params = payload.get("params")
arguments = params.get("arguments") if isinstance(params, dict) else None
if isinstance(arguments, dict):
mode = arguments.get("mode")
mode = arguments.get("mode", DEFAULT_SEARCH_MODE)
tool = body_tool or header_tool
tool = tool if tool in KNOWN_TOOLS else None
# Hybrid retrieval also runs the dense embedding search upstream.
Expand Down
6 changes: 4 additions & 2 deletions mcp_server/src/pwc_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
)
# Hosted ceiling on rows per response (SPEC.md); the CLI allows up to 100.
MAX_ROWS = 25
# Matches `pwc search --mode`; app.py counts omitted modes with this value.
DEFAULT_SEARCH_MODE = "hybrid"
# CLI lookup failures the caller can act on (an unknown or ambiguous name, an
# unconfirmed filter). Transport, HTTP, and response-shape failures stay generic.
CLIENT_FACING_ERRORS = (
Expand Down Expand Up @@ -382,14 +384,14 @@ def run(tool: str, **parameters: Any) -> Any:
@server.tool(annotations=READ_ONLY, structured_output=True)
def search_papers(
query: Query,
mode: Literal["hybrid", "keyword", "semantic"] = "keyword",
mode: Literal["hybrid", "keyword", "semantic"] = DEFAULT_SEARCH_MODE,
page: Page = 1,
limit: Limit = 10,
published_after: IsoDate | None = None,
published_before: IsoDate | None = None,
has_official_implementation: bool = False,
) -> PaperPage:
"""Search papers by title, topic, author, or arXiv ID (`pwc search`). Broad discovery only: for best, top, or state-of-the-art model questions start with get_task, list_benchmarks, and get_benchmark, which return leaderboard evidence that search cannot."""
"""Search papers by title, topic, author, or arXiv ID (`pwc search`). Broad discovery only: for best, top, or state-of-the-art model questions start with get_task, list_benchmarks, and get_benchmark, which return leaderboard evidence that search cannot. Mode defaults to hybrid like `pwc search`; use keyword for exact terminology or to stay under the semantic search rate limit."""
_validate_date_range(published_after, published_before)
result = _paper_page(
run(
Expand Down
47 changes: 46 additions & 1 deletion mcp_server/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_health_and_browser_origin_policy_are_explicit():
assert health.json() == {
"status": "ok",
"service": "pwc-mcp",
"version": "0.2.2",
"version": "0.2.3",
"protocol": "2025-11-25",
}
assert rejected.status_code == 403
Expand Down Expand Up @@ -294,6 +294,51 @@ def test_hybrid_search_counts_toward_the_semantic_limit():
assert limited.status_code == 429


def test_omitted_search_mode_defaults_to_hybrid_and_counts_toward_the_semantic_limit():
app = create_app(
StubCatalog(),
allowed_hosts=["testserver"],
semantic_limit=1,
)
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": "search_papers", "arguments": {"query": "attention"}},
}

with TestClient(app) as client:
first = client.post("/mcp", json=body)
limited = client.post("/mcp", json=body)

assert first.status_code != 429
assert limited.status_code == 429


def test_keyword_search_does_not_count_toward_the_semantic_limit():
app = create_app(
StubCatalog(),
allowed_hosts=["testserver"],
semantic_limit=1,
)
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_papers",
"arguments": {"query": "attention", "mode": "keyword"},
},
}

with TestClient(app) as client:
first = client.post("/mcp", json=body)
second = client.post("/mcp", json=body)

assert first.status_code != 429
assert second.status_code != 429


def test_rate_limits_are_env_configurable_with_hosted_defaults(monkeypatch):
from pwc_mcp.app import (
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
Expand Down
4 changes: 4 additions & 0 deletions mcp_server/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ async def exercise():
"keyword",
"semantic",
]
# Same default as `pwc search --mode`.
assert (
tools["search_papers"].input_schema["properties"]["mode"]["default"] == "hybrid"
)
assert result.is_error is False
assert result.structured_content == {
"schema_version": "v1",
Expand Down
2 changes: 1 addition & 1 deletion mcp_server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading