Skip to content

Anonymous MCP access to the public corpus (own switch + per-IP rate limit) #96

Description

@gangtao

Follow-up to #94 / PR #95, which shipped anonymous web chat over the public corpus and deliberately left MCP closed (/mcp without a token is still 401).

Goal

A coding agent with no token can use the graph tools over MCP for the public entries only:

claude mcp add --transport http timeplus-knowledge https://<host>/mcp     # no --header

Already in place (from #95)

  • The anonymous principal; auth.resolve_scope() returns the enabled public entry keys for it, and the MCP guard (mcp_http.make_guard) calls exactly that — so once an anonymous caller reaches a tool call, all six tools are scoped to public entries with no new filtering code.
  • read_source requires source:view, which anonymous never holds → raw source stays closed automatically.
  • Isolation tests for MCP scope already exist (tests/test_mcp_http.py); they only need an anonymous caller added.

Change

  1. Own switch: TPK_ANONYMOUS_MCP / [server].anonymous_mcp (default false), independent of TPK_ANONYMOUS_ACCESS, so either can be on without the other.
  2. McpAuth: no Authorization header + switch on → the anonymous principal. A bad/expired/non-tpk_ token is still 401 (never downgraded to anonymous — same rule as chat). Denial logging unchanged; tool-call log line says user=anonymous.
  3. Capability gate: /mcp requires explore today and anonymous holds only chat. Do not widen anonymous's capabilities; instead let McpAuth admit the anonymous principal explicitly when the switch is on (the gate stays explore for real users). Keep anonymous out of read_source (already true).
  4. Per-IP rate limit on /mcp — required before this goes on in production. MCP tools make no LLM calls, so the token budget does not apply: the exposure is database load from an unbounded loop of search_entities / neighbors calls (each call is bounded, the loop is not). Limiter keyed on client IP (needs the real client address — check the NLB / proxy-protocol setup on the k8s deployment; behind the NLB every request may otherwise share one address), e.g. N tool calls per minute per IP, 429 with Retry-After. Same limiter should apply to anonymous /chat and can later cover authenticated MCP (Remote MCP follow-ups: audit trail, rate limiting, token hygiene, OAuth #80).
  5. initialize's serverInfo/instructions mention that the unauthenticated view covers public documentation only.

Acceptance

  • Switch off: /mcp without a token is 401 (today's tests unchanged).
  • Switch on: initialize, tools/list, search_entities, get_entity, neighbors, path_between, list_communities return only public-entry data for an anonymous caller — per-tool isolation tests, including that an internal entity's id/name never appears anywhere in a response (hints included); read_source is a tool error; a bad token is still 401; a real token still gets its own scope.
  • Rate limit: the (N+1)th call within the window is 429 with Retry-After; tests with a fake clock.
  • Docs: README remote-MCP section, FEATURES §5, k8s README (client-IP caveat), config samples.

Related: #94 (decisions), #80 (MCP rate limiting / audit — share the limiter).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions