MCP#

pip install "needtoknow[mcp]"

add_search_tool gives an MCP server a search tool that returns only what the calling user may read. Any MCP client, such as an AI assistant or an agent framework, can then search your documents with the user’s own permissions. It uses the MCP Python SDK, version 2.3 or later.

from mcp.server import MCPServer
from mcp.server.auth.settings import AuthSettings
from pydantic import AnyHttpUrl

from needtoknow import PermissionedRetriever, Subject
from needtoknow.mcp import add_search_tool

server = MCPServer(
    "Company knowledge",
    token_verifier=verifier,  # checks the bearer token on every request
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://login.example.com"),
        resource_server_url=AnyHttpUrl("https://knowledge.example.com/mcp"),
        required_scopes=["knowledge:search"],
        validate_token_resource=True,
    ),
)


def subject_for(token):
    """The user the verified token was issued for, with the groups the directory gives them."""
    return Subject(token.subject, directory.groups_of(token.subject))


add_search_tool(server, PermissionedRetriever(backend, authorizer), subject_for)
server.run(transport="streamable-http")

verifier is your TokenVerifier: it checks a JWT’s signature or asks your identity provider’s introspection endpoint, and returns the token’s details. The SDK’s authorization guide shows how to write one; directory stands for wherever your groups live.

Whose permissions apply#

The model chooses what to search for, never whose permissions apply:

  • The tool’s only arguments are query and limit. There is no user, group or filter argument, and an extra argument a model invents is ignored.

  • The subject comes from the access token the server verified for the request, through your subject_for function. Build it from what the token proves, such as its subject and claims, or from a directory lookup on that identity (security model).

  • A call without a verified token is refused with a protocol error, and nothing is searched. That covers a server run over stdio, and an HTTP server started without a token verifier.

  • subject_for may be async, and must return a Subject; anything else fails the call.

When an agent acts for a user, the token should be issued for that user (for example through OAuth token exchange), so token.subject is the person and the agent’s own identity is in token.client_id or an act claim in token.claims. Check those in subject_for if only some agents may search.

For a server only one person can reach, such as a local stdio server on their own machine, pass that person’s Subject instead of a function:

add_search_tool(server, retriever, Subject("alice", ["eng"]))
server.run()  # stdio

What the tool returns#

The tool is marked read-only, and returns structured results:

{
  "results": [{"id": "handbook-3", "text": "...", "score": 0.82, "metadata": {}}],
  "complete": true
}
  • metadata is empty unless you name keys to include: add_search_tool(..., metadata=["source", "title"]). Permission fields describe who may read what, so they are left out by default.

  • complete is RetrievalResult.complete: false when passages the caller may read could be missing.

  • limit defaults to 5 and is capped at max_limit (20); both are arguments of add_search_tool. name and description set the tool’s name and the description the model reads.

Failures#

  • The permission service failing ends the call with a tool error the model can read (“the permission check could not be completed, so nothing was returned”), without the internal details, which go to your logs and audit events.

  • Any other failure, such as subject_for raising, reaches the model only as a failed call; the traceback stays in the server’s log.

Everything else about retrieval applies as for any caller: the retriever’s strategy, caching, audit events and metrics.