Skip to content

Add local MCP server for public REST reads - #1013

Open
nolanefe wants to merge 2 commits into
OWASP:mainfrom
nolanefe:feat/mcp-public-rest-tools
Open

Add local MCP server for public REST reads#1013
nolanefe wants to merge 2 commits into
OWASP:mainfrom
nolanefe:feat/mcp-public-rest-tools

Conversation

@nolanefe

@nolanefe nolanefe commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Implements the maintainer-approved first phase of #1003: a local stdio MCP server exposing a bounded set of public OpenCRE REST reads.

The MCP surface contains nine tools:

  • get_cre_by_id
  • get_cre_by_name
  • get_node
  • get_documents_by_tag
  • text_search
  • list_root_cres
  • list_all_cres
  • list_standards
  • list_ga_standards

Each tool is backed by its corresponding /rest/v1 endpoint. Tool exposure comes from an explicit public-read allowlist, while argument names/types/requiredness are derived from the committed OpenAPI specification.

Design

  • local stdio MCP via the official Python MCP SDK
  • OpenAPI-derived input schemas for allowlisted operations
  • REST remains the execution/security boundary
  • fixed GET route templates only; no generic REST proxy
  • OPENCRE_BASE_URL is server-side configuration and cannot be supplied by tool callers
  • OpenAPI membership alone does not expose a tool
  • runtime JSON Schema validation before REST dispatch
  • get_node restricts ntype to CRE document types to prevent collisions with routes such as /rest/v1/user/resources
  • no cookies, session, PAT, OAuth, or environment/netrc credential forwarding
  • unexpected internal exceptions are logged server-side without leaking details to MCP clients

Scope

Per the direction in #1003, this PR intentionally does not add authentication.

Deferred to follow-up work:

  • MyOpenCRE /rest/v1/user/resources
  • /rest/v1/completion
  • admin/import routes
  • credential/session forwarding
  • node section/subsection route variants
  • map-analysis/job endpoints
  • CSV/config/health/deeplink surfaces

docs/api/mcp.md documents local startup, Cursor configuration, the tool-to-REST mapping, security boundary, and these deliberate parity gaps.

Validation

  • MCP-focused tests: 26 passed
  • OpenAPI guardrail: passed
  • make lint: passed
  • mypy is not currently clean: the focused MCP invocation reports missing third-party stubs (jsonschema, networkx), request-handler context typing in MCP tests, and errors reached through existing imported modules
  • repository-wide make mypy also remains nonzero because of existing repository typing debt; this PR does not attempt unrelated typing cleanup
  • make test: 767 tests passed, 3 skipped
  • git diff --check: clean
  • E2E workflow: GitHub reports No jobs were run because the upstream .github/workflows/e2e.yml is currently fully commented out and contains no executable jobs; this is existing repository configuration, not a failure introduced by this PR.

Part of #1003

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad0f04ca-b847-4a50-a845-9b569bdac858

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd768b and 110b069.

📒 Files selected for processing (4)
  • application/mcp/openapi_loader.py
  • application/mcp/rest_client.py
  • application/mcp/server.py
  • application/tests/mcp_public_tools_test.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • application/mcp/server.py
  • application/mcp/openapi_loader.py
  • application/mcp/rest_client.py

Summary by CodeRabbit

  • New Features
    • Added a local stdio MCP server exposing nine approved, read-only REST tools.
    • Added OpenAPI-derived input schemas, argument validation, and structured tool responses.
    • Added secure REST communication with restricted methods, validated paths, and controlled error handling.
  • Documentation
    • Added setup, configuration, Cursor integration, tool mappings, and security guidance.
    • Linked the new MCP documentation from the README.
  • Tests
    • Added comprehensive coverage for tool discovery, validation, invocation, responses, and error handling.

Walkthrough

Added a local stdio MCP server for nine allowlisted OpenCRE public REST GET operations. The implementation derives schemas from OpenAPI, validates REST requests, exposes MCP dispatch, adds tests, and documents setup and scope.

Changes

Public MCP server

Layer / File(s) Summary
Tool catalog and OpenAPI schemas
application/mcp/catalog.py, application/mcp/openapi_loader.py, application/mcp/__init__.py
Defines nine public tools and generates their MCP input schemas from OpenAPI operations.
Secure REST execution
application/mcp/rest_client.py
Validates arguments, restricts requests to fixed-base GET operations, sanitizes paths, encodes queries, and parses responses.
MCP server runtime and entry point
application/mcp/server.py, application/mcp/__main__.py
Lists tools, dispatches calls through the REST client, maps errors, and starts the stdio transport.
Behavior and security validation
application/tests/mcp_public_tools_test.py
Tests tool restrictions, schema generation, REST parity, server dispatch, error handling, and configuration.
Project setup and usage documentation
requirements-dev.txt, docs/api/mcp.md, README.md, .gitignore
Adds the MCP dependency, usage documentation, README guidance, and documentation inclusion exception.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • OWASP/OpenCRE issue 1003 — This PR adds the REST-backed MCP interface described by the issue for a public read-only subset.

Possibly related PRs

  • OWASP/OpenCRE#978 — This PR adds the MCP dependency to the development requirements introduced by PR #978.

Suggested reviewers: pa04rth, paoga87, robvanderveer, northdpole

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a local MCP server for public REST reads.
Description check ✅ Passed The description directly explains the MCP server, its nine public REST tools, design, scope, security boundaries, and validation results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
application/mcp/rest_client.py (1)

296-318: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider narrowing the JSON decode fallback.

Line 302 catches every exception to fall back to raw text. Ruff flags this as BLE001. ValueError (which json.JSONDecodeError subclasses) covers both requests and the Flask test response wrapper. A narrower catch keeps genuine adapter faults visible.

♻️ Proposed change
     try:
         data = response.json()
-    except Exception:
+    except ValueError:
         data = text
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/mcp/rest_client.py` around lines 296 - 318, Update
_parse_response so the response.json() fallback catches only ValueError instead
of every exception, preserving raw-text fallback for JSON decoding failures
while allowing genuine adapter errors to propagate.

Source: Linters/SAST tools

application/mcp/server.py (1)

61-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the precomputed schemas and remove the duplicated lookups.

Lines 68-72 perform four redundant steps per call: list_tool_names() rebuilds a list for a membership test, get_tool(name) runs twice, and operation_input_schema re-walks the OpenAPI parameters even though build_server already resolved every schema into schemas. client.call_tool performs the same catalog lookup and schema resolution again. A single membership test against schemas keeps the same behavior with less work.

♻️ Proposed simplification
         try:
-            if name not in list_tool_names():
+            if name not in schemas:
                 raise RestRequestError(f"Unknown MCP tool: {name}")
-            # Ensure allowlist entry still matches OpenAPI before calling REST.
-            get_tool(name)
-            operation_input_schema(get_tool(name))
             result = client.call_tool(name, arguments)

Drop the now-unused get_tool, list_tool_names, and operation_input_schema imports if nothing else uses them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/mcp/server.py` around lines 61 - 79, Update on_call_tool to
validate tool existence with a direct membership check against the precomputed
schemas mapping, then call client.call_tool without the redundant
list_tool_names, get_tool, or operation_input_schema lookups. Remove those
imports if no other code uses them, while preserving the existing unknown-tool
error behavior.
application/mcp/openapi_loader.py (1)

37-59: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider a recursion guard for $ref cycles.

_resolve_ref follows references recursively. If the committed OpenAPI document ever contains a cyclic $ref, this raises RecursionError at server startup instead of a clear OpenAPILookupError. A visited-ref set keeps the failure diagnosable.

♻️ Proposed guard
-def _resolve_ref(spec: Dict[str, Any], node: Any) -> Any:
+def _resolve_ref(
+    spec: Dict[str, Any], node: Any, _seen: Optional[Set[str]] = None
+) -> Any:
     """Resolve local `#/components/`... refs one level deep as needed."""
+    seen = set(_seen or ())
     if not isinstance(node, dict):
         return node
     if "$ref" not in node:
-        return {key: _resolve_ref(spec, value) for key, value in node.items()}
+        return {key: _resolve_ref(spec, value, seen) for key, value in node.items()}
     ref = node["$ref"]
     if not isinstance(ref, str) or not ref.startswith("`#/`"):
         raise OpenAPILookupError(f"Unsupported OpenAPI $ref: {ref}")
+    if ref in seen:
+        raise OpenAPILookupError(f"Circular OpenAPI $ref: {ref}")
+    seen.add(ref)

Pass seen through the remaining recursive call on the merged result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/mcp/openapi_loader.py` around lines 37 - 59, Update _resolve_ref
to track the currently visited $ref values through recursive resolution,
including the recursive call after merging sibling keywords. Before following a
reference, detect if it is already in the set and raise OpenAPILookupError with
the reference identifier; otherwise add it and propagate the set through
subsequent _resolve_ref calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/mcp/rest_client.py`:
- Around line 172-178: Update the cookie cleanup block in the REST client method
containing _parse_response so failures from self._session.cookies.clear() are
logged instead of silently ignored. Add a module-level logging import and logger
initialized with __name__, then use it in the exception handler while preserving
the existing response parsing and cookie-clearing behavior.

In `@application/tests/mcp_public_tools_test.py`:
- Around line 51-65: Scope the INSECURE_REQUESTS environment change in setUp
using the imported patch.dict mechanism, or otherwise restore its original value
during tearDown. Update setUp and tearDown so later tests do not inherit "True",
while preserving the existing application and database cleanup flow.

---

Nitpick comments:
In `@application/mcp/openapi_loader.py`:
- Around line 37-59: Update _resolve_ref to track the currently visited $ref
values through recursive resolution, including the recursive call after merging
sibling keywords. Before following a reference, detect if it is already in the
set and raise OpenAPILookupError with the reference identifier; otherwise add it
and propagate the set through subsequent _resolve_ref calls.

In `@application/mcp/rest_client.py`:
- Around line 296-318: Update _parse_response so the response.json() fallback
catches only ValueError instead of every exception, preserving raw-text fallback
for JSON decoding failures while allowing genuine adapter errors to propagate.

In `@application/mcp/server.py`:
- Around line 61-79: Update on_call_tool to validate tool existence with a
direct membership check against the precomputed schemas mapping, then call
client.call_tool without the redundant list_tool_names, get_tool, or
operation_input_schema lookups. Remove those imports if no other code uses them,
while preserving the existing unknown-tool error behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93aff805-2ca6-4c8e-99fe-2b82d7d93e46

📥 Commits

Reviewing files that changed from the base of the PR and between 71f6c81 and 7cd768b.

📒 Files selected for processing (11)
  • .gitignore
  • README.md
  • application/mcp/__init__.py
  • application/mcp/__main__.py
  • application/mcp/catalog.py
  • application/mcp/openapi_loader.py
  • application/mcp/rest_client.py
  • application/mcp/server.py
  • application/tests/mcp_public_tools_test.py
  • docs/api/mcp.md
  • requirements-dev.txt

Comment thread application/mcp/rest_client.py
Comment thread application/tests/mcp_public_tools_test.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant