From 9ca6dd58957dfe9731c9591ed293f8748d6a07ee Mon Sep 17 00:00:00 2001 From: shuanbao zhu Date: Wed, 29 Apr 2026 21:40:28 +0700 Subject: [PATCH] docs: add Lore Context MCP server example Adds a FastMCP server example that wraps the Lore Context REST API, exposing governed agent memory tools (search, write, get, list, forget) for any MCP-compatible client. --- examples/lore_context_server/.fastmcp.json | 7 + examples/lore_context_server/README.md | 124 +++++++++++ examples/lore_context_server/pyproject.toml | 10 + examples/lore_context_server/server.py | 235 ++++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 examples/lore_context_server/.fastmcp.json create mode 100644 examples/lore_context_server/README.md create mode 100644 examples/lore_context_server/pyproject.toml create mode 100644 examples/lore_context_server/server.py diff --git a/examples/lore_context_server/.fastmcp.json b/examples/lore_context_server/.fastmcp.json new file mode 100644 index 000000000..df56de039 --- /dev/null +++ b/examples/lore_context_server/.fastmcp.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", + "entrypoint": "server.py", + "environment": { + "dependencies": ["httpx"] + } +} diff --git a/examples/lore_context_server/README.md b/examples/lore_context_server/README.md new file mode 100644 index 000000000..58ac3fc28 --- /dev/null +++ b/examples/lore_context_server/README.md @@ -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). diff --git a/examples/lore_context_server/pyproject.toml b/examples/lore_context_server/pyproject.toml new file mode 100644 index 000000000..878dac9b1 --- /dev/null +++ b/examples/lore_context_server/pyproject.toml @@ -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", +] diff --git a/examples/lore_context_server/server.py b/examples/lore_context_server/server.py new file mode 100644 index 000000000..8248dbd38 --- /dev/null +++ b/examples/lore_context_server/server.py @@ -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, + 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()