Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/lore_context_server/.fastmcp.json
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"]
}
}
124 changes: 124 additions & 0 deletions examples/lore_context_server/README.md
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).
10 changes: 10 additions & 0 deletions examples/lore_context_server/pyproject.toml
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",
]
235 changes: 235 additions & 0 deletions examples/lore_context_server/server.py
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", "")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail fast when LORE_API_KEY is unset

The server treats LORE_API_KEY as 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 sends Authorization: Bearer and 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 👍 / 👎.


# ---------------------------------------------------------------------------
# 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid invalid default scope in memory_write

memory_write defaults scope to "project" while project_id remains optional, but this same function documents that project_id is required for project scope. That makes the simplest valid MCP call shape (content only) produce a guaranteed 4xx from the Lore API, so the tool’s defaults are internally inconsistent and lead to immediate runtime failures for clients that rely on default arguments.

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()
Loading