-
Notifications
You must be signed in to change notification settings - Fork 2k
docs(examples): add Lore Context MCP server example #4082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", | ||
| "entrypoint": "server.py", | ||
| "environment": { | ||
| "dependencies": ["httpx"] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # Lore Context MCP Server | ||
|
|
||
| A [FastMCP](https://github.com/PrefectHQ/fastmcp) server that wraps the | ||
| [Lore Context](https://github.com/nousresearch/lore) REST API, exposing | ||
| governed agent memory as MCP tools. | ||
|
|
||
| Any MCP-compatible client (Claude Desktop, Cursor, Codex, etc.) can search, | ||
| write, read, list, and forget memories through this server. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Python ≥ 3.10 | ||
| - A running [Lore Context API](http://127.0.0.1:3000) instance | ||
| - An API key with at least `reader` role (write/forget require `writer`/`admin`) | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```bash | ||
| # Install dependencies | ||
| pip install fastmcp httpx | ||
|
|
||
| # Set environment variables | ||
| export LORE_API_URL=http://127.0.0.1:3000 # your Lore API URL | ||
| export LORE_API_KEY=your-api-key-here | ||
|
|
||
| # Run the server (stdio transport, default for MCP) | ||
| python server.py | ||
|
|
||
| # Or use the FastMCP CLI | ||
| fastmcp run server.py | ||
| ``` | ||
|
|
||
| ## Tools | ||
|
|
||
| | Tool | Mutates | Description | | ||
| |-----------------|---------|------------------------------------------------| | ||
| | `memory_search` | no | Semantic search over Lore memories | | ||
| | `memory_write` | yes | Write a new governed memory | | ||
| | `memory_get` | no | Fetch a single memory by id | | ||
| | `memory_list` | no | List memories with optional filters | | ||
| | `memory_forget` | yes | Soft-delete or hard-delete memories | | ||
|
|
||
| ## Configuration | ||
|
|
||
| Set these environment variables before running: | ||
|
|
||
| | Variable | Default | Description | | ||
| |----------------|--------------------------|----------------------------------| | ||
| | `LORE_API_URL` | `http://127.0.0.1:3000` | Base URL of the Lore Context API | | ||
| | `LORE_API_KEY` | *(none – required)* | Bearer token for authentication | | ||
|
|
||
| ## Using with Claude Desktop | ||
|
|
||
| Add to your `claude_desktop_config.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "lore-context": { | ||
| "command": "python", | ||
| "args": ["/path/to/examples/lore_context_server/server.py"], | ||
| "env": { | ||
| "LORE_API_URL": "http://127.0.0.1:3000", | ||
| "LORE_API_KEY": "your-api-key-here" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Using with the FastMCP CLI | ||
|
|
||
| ```bash | ||
| # Inspect the server | ||
| fastmcp inspect server.py | ||
|
|
||
| # Run with streamable HTTP transport | ||
| fastmcp run server.py --transport streamable-http --port 8000 | ||
| ``` | ||
|
|
||
| ## Example Usage (Python Client) | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from fastmcp import Client | ||
|
|
||
| async def main(): | ||
| async with Client("server.py") as client: | ||
| # List available tools | ||
| tools = await client.list_tools() | ||
| print([t.name for t in tools]) | ||
|
|
||
| # Search memories | ||
| results = await client.call_tool("memory_search", { | ||
| "query": "FastMCP server architecture", | ||
| "top_k": 5, | ||
| }) | ||
| print(results) | ||
|
|
||
| # Write a memory | ||
| result = await client.call_tool("memory_write", { | ||
| "content": "The Lore Context server uses httpx for REST calls.", | ||
| "scope": "project", | ||
| "project_id": "fastmcp-demo", | ||
| }) | ||
| print(result) | ||
|
|
||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| ## Lore Context REST API | ||
|
|
||
| This server proxies these Lore Context REST endpoints: | ||
|
|
||
| | MCP Tool | HTTP Method | REST Endpoint | | ||
| |------------------|-------------|-----------------------| | ||
| | `memory_search` | POST | `/v1/memory/search` | | ||
| | `memory_write` | POST | `/v1/memory/write` | | ||
| | `memory_get` | GET | `/v1/memory/{id}` | | ||
| | `memory_list` | GET | `/v1/memory/list` | | ||
| | `memory_forget` | POST | `/v1/memory/forget` | | ||
|
|
||
| For the full API reference, see the | ||
| [Lore Context API docs](https://github.com/nousresearch/lore/blob/main/docs/api-reference.md). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [project] | ||
| name = "lore-context-mcp-server" | ||
| version = "0.1.0" | ||
| description = "FastMCP server wrapping the Lore Context REST API for governed agent memory" | ||
| readme = "README.md" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
| "fastmcp>=2.0.0", | ||
| "httpx>=0.27.0", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| """ | ||
| FastMCP Lore Context Server | ||
|
|
||
| Wraps the Lore Context REST API as MCP tools, giving any MCP-compatible | ||
| client access to governed agent memory (search, write, read, list, forget). | ||
|
|
||
| Configuration (environment variables): | ||
| LORE_API_URL – Base URL of the Lore Context API (default: http://127.0.0.1:3000) | ||
| LORE_API_KEY – API key for authentication (required) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import Any | ||
|
|
||
| import httpx | ||
| from fastmcp import FastMCP | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Configuration | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| LORE_API_URL = os.environ.get("LORE_API_URL", "http://127.0.0.1:3000").rstrip("/") | ||
| LORE_API_KEY = os.environ.get("LORE_API_KEY", "") | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # FastMCP server | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| mcp = FastMCP("Lore Context Server") | ||
|
|
||
|
|
||
| def _headers() -> dict[str, str]: | ||
| """Build auth headers for every Lore REST call.""" | ||
| return { | ||
| "Authorization": f"Bearer {LORE_API_KEY}", | ||
| "Content-Type": "application/json", | ||
| } | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Tools | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @mcp.tool | ||
| async def memory_search( | ||
| query: str, | ||
| project_id: str | None = None, | ||
| top_k: int | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Search Lore memories by semantic similarity. | ||
|
|
||
| Args: | ||
| query: Natural-language search query. | ||
| project_id: Optional project scope filter. | ||
| top_k: Maximum number of hits to return (1-100). | ||
|
|
||
| Returns: | ||
| A dict with a ``hits`` key containing matching memory records. | ||
| """ | ||
| body: dict[str, Any] = {"query": query} | ||
| if project_id is not None: | ||
| body["project_id"] = project_id | ||
| if top_k is not None: | ||
| body["top_k"] = top_k | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| resp = await client.post( | ||
| f"{LORE_API_URL}/v1/memory/search", | ||
| json=body, | ||
| headers=_headers(), | ||
| timeout=30, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
|
|
||
| @mcp.tool | ||
| async def memory_write( | ||
| content: str, | ||
| scope: str = "project", | ||
| project_id: str | None = None, | ||
|
Comment on lines
+83
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| memory_type: str | None = None, | ||
| confidence: float | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Write a new governed memory into Lore. | ||
|
|
||
| The memory may enter a review queue depending on governance policy. | ||
|
|
||
| Args: | ||
| content: The memory content to store. | ||
| scope: Memory scope – one of "user", "project", "repo", "team", "org". | ||
| project_id: Required when scope is "project". | ||
| memory_type: Optional memory type label. | ||
| confidence: Optional confidence score (0.0-1.0). | ||
|
|
||
| Returns: | ||
| A dict with ``memory`` (the created record) and ``reviewRequired``. | ||
| """ | ||
| body: dict[str, Any] = {"content": content, "scope": scope} | ||
| if project_id is not None: | ||
| body["project_id"] = project_id | ||
| if memory_type is not None: | ||
| body["memory_type"] = memory_type | ||
| if confidence is not None: | ||
| body["confidence"] = confidence | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| resp = await client.post( | ||
| f"{LORE_API_URL}/v1/memory/write", | ||
| json=body, | ||
| headers=_headers(), | ||
| timeout=30, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
|
|
||
| @mcp.tool | ||
| async def memory_get(memory_id: str) -> dict[str, Any]: | ||
| """Fetch a single memory record by its id. | ||
|
|
||
| Args: | ||
| memory_id: The unique memory identifier. | ||
|
|
||
| Returns: | ||
| A dict with a ``memory`` key containing the full record. | ||
| """ | ||
| async with httpx.AsyncClient() as client: | ||
| resp = await client.get( | ||
| f"{LORE_API_URL}/v1/memory/{memory_id}", | ||
| headers=_headers(), | ||
| timeout=30, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
|
|
||
| @mcp.tool | ||
| async def memory_list( | ||
| project_id: str | None = None, | ||
| scope: str | None = None, | ||
| status: str | None = None, | ||
| memory_type: str | None = None, | ||
| q: str | None = None, | ||
| limit: int | None = None, | ||
| ) -> dict[str, Any]: | ||
| """List visible memories with optional filters. | ||
|
|
||
| Args: | ||
| project_id: Filter by project (required for scoped API keys). | ||
| scope: Filter by memory scope. | ||
| status: Filter by lifecycle status. | ||
| memory_type: Filter by memory type. | ||
| q: Substring filter on memory content. | ||
| limit: Maximum number of memories to return. | ||
|
|
||
| Returns: | ||
| A dict with a ``memories`` key containing the list of records. | ||
| """ | ||
| params: dict[str, Any] = {} | ||
| if project_id is not None: | ||
| params["project_id"] = project_id | ||
| if scope is not None: | ||
| params["scope"] = scope | ||
| if status is not None: | ||
| params["status"] = status | ||
| if memory_type is not None: | ||
| params["memory_type"] = memory_type | ||
| if q is not None: | ||
| params["q"] = q | ||
| if limit is not None: | ||
| params["limit"] = limit | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| resp = await client.get( | ||
| f"{LORE_API_URL}/v1/memory/list", | ||
| params=params, | ||
| headers=_headers(), | ||
| timeout=30, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
|
|
||
| @mcp.tool | ||
| async def memory_forget( | ||
| reason: str, | ||
| memory_ids: list[str] | None = None, | ||
| query: str | None = None, | ||
| project_id: str | None = None, | ||
| hard_delete: bool = False, | ||
| ) -> dict[str, Any]: | ||
| """Forget (soft-delete or hard-delete) memories. | ||
|
|
||
| Provide either ``memory_ids`` to target specific memories, or ``query`` | ||
| to match and forget semantically. Admin role required for hard deletes. | ||
|
|
||
| Args: | ||
| reason: Explanation of why the memories are being forgotten (min 8 chars). | ||
| memory_ids: Explicit list of memory ids to delete. | ||
| query: Semantic query to select memories for deletion. | ||
| project_id: Optional project scope. | ||
| hard_delete: If True, permanently erase; otherwise soft-delete. | ||
|
|
||
| Returns: | ||
| A dict with ``deleted`` count, ``memoryIds``, and ``hardDelete`` flag. | ||
| """ | ||
| body: dict[str, Any] = {"reason": reason, "hard_delete": hard_delete} | ||
| if memory_ids is not None: | ||
| body["memory_ids"] = memory_ids | ||
| if query is not None: | ||
| body["query"] = query | ||
| if project_id is not None: | ||
| body["project_id"] = project_id | ||
|
|
||
| async with httpx.AsyncClient() as client: | ||
| resp = await client.post( | ||
| f"{LORE_API_URL}/v1/memory/forget", | ||
| json=body, | ||
| headers=_headers(), | ||
| timeout=30, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Entrypoint | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| if __name__ == "__main__": | ||
| mcp.run() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server treats
LORE_API_KEYas optional by defaulting it to an empty string, even though the README and module docstring mark it as required. In that configuration every tool call sendsAuthorization: Bearerand fails later with HTTP errors, so users get a server that starts successfully but cannot perform any operation; this should be rejected at startup with a clear configuration error.Useful? React with 👍 / 👎.