From 7b6a346c13cb64fbb358d60219910519d025da3f Mon Sep 17 00:00:00 2001 From: Lawrence Sinclair Date: Sat, 20 Jun 2026 04:05:09 +0000 Subject: [PATCH] fix(mcp): correct install docs, URL encoding, docstring, and test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README: replace the not-yet-published PyPI quickstart (uvx/pip commands that fail) with the working git-install form. Clarify the Claude Code config section: add JENNER_API_KEY to the .mcp.json example and spell out the --from git+ form consistently across all snippets. server.py: encode run_id and dataset path segments with urllib.parse.quote so names containing spaces or special characters reach the API correctly. Fix validate_sas docstring — diagnostics list items are dicts {"severity": str, "message": str}, not bare sets. tests: add test_dataset_preview (was the only tool with zero coverage) and test_get_run_url_encodes_run_id to pin the URL encoding behaviour. 14/14 pass. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 32 ++++++++++++++++++-------------- src/jenner_sas_mcp/server.py | 7 ++++--- tests/test_server.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index be03a4e..33ce73e 100644 --- a/README.md +++ b/README.md @@ -21,25 +21,20 @@ standard SAS (DATA steps, PROCs, macros) and it runs as-is. ## Quickstart -The server speaks MCP over stdio. The fastest way to run it is with -[`uv`](https://docs.astral.sh/uv/) (no manual install step — `uvx` fetches and -runs it on demand): +The server speaks MCP over stdio. Install directly from this repository with +[`uv`](https://docs.astral.sh/uv/) (no manual install step): ```bash -uvx jenner-sas-mcp +uvx --from git+https://github.com/JennerAnalytics/jenner-sas-mcp jenner-sas-mcp ``` -Or install it into an environment with pip: +Or install with pip: ```bash -pip install jenner-sas-mcp +pip install git+https://github.com/JennerAnalytics/jenner-sas-mcp jenner-sas-mcp # starts the stdio server ``` -> Until the package is published to PyPI, install from this repository: -> `uvx --from git+https://github.com/JennerAnalytics/jenner-sas-mcp jenner-sas-mcp` -> or `pip install git+https://github.com/JennerAnalytics/jenner-sas-mcp`. - ## Configuration All configuration is via environment variables: @@ -63,7 +58,7 @@ Windows: `%APPDATA%\Claude\claude_desktop_config.json`) and add: "mcpServers": { "jenner-sas": { "command": "uvx", - "args": ["jenner-sas-mcp"], + "args": ["--from", "git+https://github.com/JennerAnalytics/jenner-sas-mcp", "jenner-sas-mcp"], "env": { "JENNER_API_KEY": "your-api-key-optional" } @@ -77,16 +72,25 @@ Restart Claude Desktop. The `jenner-sas` tools appear in the tools menu. ### Claude Code +Add the server with your API key (omit `--env` if running anonymously): + ```bash -claude mcp add jenner-sas --env JENNER_API_KEY=your-key -- uvx jenner-sas-mcp +claude mcp add jenner-sas --env JENNER_API_KEY=your-key -- uvx --from git+https://github.com/JennerAnalytics/jenner-sas-mcp jenner-sas-mcp ``` -Or, for a project-scoped server checked into the repo, add it to `.mcp.json`: +For a project-scoped server checked into the repo, add to `.mcp.json` (set +`JENNER_API_KEY` in your shell environment or via `claude mcp add` above): ```json { "mcpServers": { - "jenner-sas": { "command": "uvx", "args": ["jenner-sas-mcp"] } + "jenner-sas": { + "command": "uvx", + "args": ["--from", "git+https://github.com/JennerAnalytics/jenner-sas-mcp", "jenner-sas-mcp"], + "env": { + "JENNER_API_KEY": "your-api-key-optional" + } + } } } ``` diff --git a/src/jenner_sas_mcp/server.py b/src/jenner_sas_mcp/server.py index 93d50c1..cb94e75 100644 --- a/src/jenner_sas_mcp/server.py +++ b/src/jenner_sas_mcp/server.py @@ -23,6 +23,7 @@ import os from typing import Any +from urllib.parse import quote as _quote import httpx from mcp.server.fastmcp import FastMCP @@ -216,7 +217,7 @@ async def validate_sas(script: str) -> dict[str, Any]: script: The SAS/Jenner source to validate. Returns: - ``{"valid": bool, "diagnostics": [{"severity", "message"}, ...]}``. + ``{"valid": bool, "diagnostics": [{"severity": str, "message": str}, ...]}``. An invalid script is a normal result, not an error — read the diagnostics to see what to fix. """ @@ -253,7 +254,7 @@ async def get_run(run_id: str, access_token: str) -> dict[str, Any]: try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.get( - f"{_api_url()}/v1/run/{run_id}", + f"{_api_url()}/v1/run/{_quote(run_id, safe='')}", params={"token": access_token}, headers=_auth_headers(), ) @@ -290,7 +291,7 @@ async def dataset_preview( try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.get( - f"{_api_url()}/v1/run/{run_id}/datasets/{dataset}", + f"{_api_url()}/v1/run/{_quote(run_id, safe='')}/datasets/{_quote(dataset, safe='')}", params={"token": access_token}, headers=_auth_headers(), ) diff --git a/tests/test_server.py b/tests/test_server.py index 8835e46..1151780 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -190,6 +190,25 @@ async def test_get_run_returns_unclipped(monkeypatch): assert cap["params"]["token"] == "tok" +@pytest.mark.asyncio +async def test_dataset_preview(monkeypatch): + monkeypatch.delenv("JENNER_API_KEY", raising=False) + payload = {"rows": 2, "columns": ["id", "x"], "data": [{"id": 1, "x": 9}]} + with _stub_httpx(payload) as cap: + out = await server.dataset_preview("r_42", "my dataset", "tok") + assert out["rows"] == 2 + assert cap["url"].endswith("/v1/run/r_42/datasets/my%20dataset") + assert cap["params"]["token"] == "tok" + + +@pytest.mark.asyncio +async def test_get_run_url_encodes_run_id(monkeypatch): + monkeypatch.delenv("JENNER_API_KEY", raising=False) + with _stub_httpx({"run_id": "r/1", "exit_code": 0, "log": "", "output": ""}) as cap: + await server.get_run("r/1", "tok") + assert "/r%2F1" in cap["url"] + + @pytest.mark.asyncio async def test_api_url_override(monkeypatch): monkeypatch.setenv("JENNER_API_URL", "http://localhost:3000/")