From 328bc8d991112ced1fc768ad8aefbd518dfedd5b Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:11:55 -0600 Subject: [PATCH 1/9] feat(examples): add postgres-knowledge-server with authorization middleware --- .../postgres-knowledge-server/README.md | 71 ++++++ .../mcp_postgres_server/__init__.py | 0 .../mcp_postgres_server/__main__.py | 3 + .../mcp_postgres_server/server.py | 215 ++++++++++++++++++ .../postgres-knowledge-server/pyproject.toml | 43 ++++ 5 files changed, 332 insertions(+) create mode 100644 examples/servers/postgres-knowledge-server/README.md create mode 100644 examples/servers/postgres-knowledge-server/mcp_postgres_server/__init__.py create mode 100644 examples/servers/postgres-knowledge-server/mcp_postgres_server/__main__.py create mode 100644 examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py create mode 100644 examples/servers/postgres-knowledge-server/pyproject.toml diff --git a/examples/servers/postgres-knowledge-server/README.md b/examples/servers/postgres-knowledge-server/README.md new file mode 100644 index 000000000..7987385f1 --- /dev/null +++ b/examples/servers/postgres-knowledge-server/README.md @@ -0,0 +1,71 @@ +# Postgres Knowledge Server + +A production-grade MCP server backed by PostgreSQL with authorization middleware. + +Demonstrates: +- **Multi-tool MCP server** — knowledge store, task queue, file routing +- **Authorization middleware** — filesystem-based identity gate (no ACL database) +- **Postgres backend** — Unix socket connection, no host/port exposure +- **Portless stdio transport** — no HTTP server, no open ports + +## Architecture + +``` +MCP Client (Claude Code, etc.) + │ stdio + ▼ + sap/sap_mcp.py ← authorization gate + FastMCP server + │ + ├── willow_store ← SQLite local store (30+ tools) + ├── postgres KB ← knowledge graph (atoms, entities) + └── kart queue ← sandboxed task executor +``` + +## Authorization Pattern + +Instead of a permission database, authorization is filesystem-based: + +```python +SAFE_ROOT = Path.home() / "Ashokoa" / "SAFE" + +def authorized(app_id: str) -> bool: + """Agent has a SAFE folder → it has access. No folder → denied.""" + folder = SAFE_ROOT / app_id + return folder.exists() and (folder / "manifest").exists() +``` + +Grant access: `mkdir -p ~/Ashokoa/SAFE/my-agent && touch ~/Ashokoa/SAFE/my-agent/manifest` +Revoke access: `rm -rf ~/Ashokoa/SAFE/my-agent` + +The filesystem shape IS the identity. No separate ACL. + +## Running + +```bash +# Install +pip install mcp psycopg2-binary + +# Configure Postgres (Unix socket — no host/port) +createdb myknowledge + +# Run +python server.py +``` + +## MCP Config (Claude Code) + +```json +{ + "mcpServers": { + "knowledge": { + "command": "python", + "args": ["/path/to/server.py"], + "env": { + "WILLOW_PG_DB": "myknowledge", + "WILLOW_PG_USER": "myuser", + "WILLOW_AGENT_NAME": "myagent" + } + } + } +} +``` diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/__init__.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/__main__.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/__main__.py new file mode 100644 index 000000000..a18c8c1ea --- /dev/null +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/__main__.py @@ -0,0 +1,3 @@ +from mcp_postgres_server.server import main + +main() diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py new file mode 100644 index 000000000..9073ab927 --- /dev/null +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -0,0 +1,215 @@ +""" +Postgres-backed MCP server with filesystem-based authorization. + +Demonstrates: +- Multi-tool MCP server (knowledge store read/write + search) +- Authorization middleware: filesystem gate, no ACL database +- Postgres backend via Unix socket (portless, no host/port exposure) +- stdio-only transport (no HTTP server) + +Usage: + Set env vars: WILLOW_PG_DB, WILLOW_PG_USER, WILLOW_SAFE_ROOT + Grant access: mkdir -p $WILLOW_SAFE_ROOT/my-app && echo '{}' > $WILLOW_SAFE_ROOT/my-app/manifest + Run: python -m mcp_postgres_server +""" + +import json +import os +from pathlib import Path + +import anyio +import click +import psycopg2 +import psycopg2.extras +from mcp import types +from mcp.server import Server, ServerRequestContext + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +PG_DB = os.environ.get("WILLOW_PG_DB", "knowledge") +PG_USER = os.environ.get("WILLOW_PG_USER", os.environ.get("USER", "postgres")) +SAFE_ROOT = Path(os.environ.get("WILLOW_SAFE_ROOT", Path.home() / "SAFE")) + + +# --------------------------------------------------------------------------- +# Authorization gate +# --------------------------------------------------------------------------- + +def authorized(app_id: str) -> bool: + """Filesystem-based authorization: folder exists → access granted. + + No permission database. The presence of the folder IS the permission. + Grant: mkdir -p $SAFE_ROOT/ && touch $SAFE_ROOT//manifest + Revoke: rm -rf $SAFE_ROOT/ + """ + if not app_id or "/" in app_id or ".." in app_id: + return False + folder = SAFE_ROOT / app_id + return folder.is_dir() and (folder / "manifest").exists() + + +# --------------------------------------------------------------------------- +# Postgres helpers +# --------------------------------------------------------------------------- + +def get_conn(): + return psycopg2.connect(dbname=PG_DB, user=PG_USER) + + +def ensure_schema(conn): + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS knowledge ( + id TEXT PRIMARY KEY, + app_id TEXT NOT NULL, + title TEXT, + body TEXT, + created TIMESTAMPTZ DEFAULT now() + ) + """) + cur.execute("CREATE INDEX IF NOT EXISTS knowledge_app ON knowledge(app_id)") + cur.execute("CREATE INDEX IF NOT EXISTS knowledge_fts ON knowledge USING gin(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')))") + conn.commit() + + +# --------------------------------------------------------------------------- +# Tools +# --------------------------------------------------------------------------- + +TOOLS = [ + types.Tool( + name="knowledge_put", + title="Store Knowledge", + description="Write a record to the knowledge base.", + input_schema={ + "type": "object", + "required": ["app_id", "id", "title", "body"], + "properties": { + "app_id": {"type": "string", "description": "Authorized app identifier"}, + "id": {"type": "string", "description": "Unique record ID"}, + "title": {"type": "string", "description": "Record title"}, + "body": {"type": "string", "description": "Record content"}, + }, + }, + ), + types.Tool( + name="knowledge_get", + title="Get Knowledge", + description="Retrieve a record by ID.", + input_schema={ + "type": "object", + "required": ["app_id", "id"], + "properties": { + "app_id": {"type": "string"}, + "id": {"type": "string"}, + }, + }, + ), + types.Tool( + name="knowledge_search", + title="Search Knowledge", + description="Full-text search across the knowledge base.", + input_schema={ + "type": "object", + "required": ["app_id", "query"], + "properties": { + "app_id": {"type": "string"}, + "query": {"type": "string"}, + "limit": {"type": "integer", "default": 10}, + }, + }, + ), +] + + +async def handle_list_tools( + ctx: ServerRequestContext, params: types.PaginatedRequestParams | None +) -> types.ListToolsResult: + return types.ListToolsResult(tools=TOOLS) + + +async def handle_call_tool( + ctx: ServerRequestContext, params: types.CallToolRequestParams +) -> types.CallToolResult: + args = params.arguments or {} + app_id = args.get("app_id", "") + + if not authorized(app_id): + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Unauthorized: no SAFE folder for '{app_id}'")], + isError=True, + ) + + try: + conn = get_conn() + ensure_schema(conn) + + if params.name == "knowledge_put": + with conn.cursor() as cur: + cur.execute( + "INSERT INTO knowledge (id, app_id, title, body) VALUES (%s, %s, %s, %s)" + " ON CONFLICT (id) DO UPDATE SET title=EXCLUDED.title, body=EXCLUDED.body", + (args["id"], app_id, args["title"], args["body"]), + ) + conn.commit() + result = {"id": args["id"], "action": "stored"} + + elif params.name == "knowledge_get": + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute("SELECT id, title, body, created FROM knowledge WHERE id=%s AND app_id=%s", + (args["id"], app_id)) + row = cur.fetchone() + result = dict(row) if row else {"error": "not_found"} + + elif params.name == "knowledge_search": + limit = min(int(args.get("limit", 10)), 50) + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + "SELECT id, title, created FROM knowledge" + " WHERE app_id=%s AND to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''))" + " @@ plainto_tsquery('english', %s)" + " LIMIT %s", + (app_id, args["query"], limit), + ) + result = [dict(r) for r in cur.fetchall()] + + else: + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")], + isError=True, + ) + + conn.close() + return types.CallToolResult( + content=[types.TextContent(type="text", text=json.dumps(result, default=str))] + ) + + except Exception as exc: + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Error: {exc}")], + isError=True, + ) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +@click.command() +def main(): + """Postgres-backed MCP knowledge server (stdio transport).""" + app = Server( + "mcp-postgres-knowledge", + on_list_tools=handle_list_tools, + on_call_tool=handle_call_tool, + ) + + from mcp.server.stdio import stdio_server + + async def arun(): + async with stdio_server() as streams: + await app.run(streams[0], streams[1], app.create_initialization_options()) + + anyio.run(arun) diff --git a/examples/servers/postgres-knowledge-server/pyproject.toml b/examples/servers/postgres-knowledge-server/pyproject.toml new file mode 100644 index 000000000..63d4e8e4a --- /dev/null +++ b/examples/servers/postgres-knowledge-server/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "mcp-postgres-knowledge" +version = "0.1.0" +description = "Postgres-backed MCP server with filesystem-based authorization" +readme = "README.md" +requires-python = ">=3.10" +authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }] +keywords = ["mcp", "postgres", "knowledge", "authorization"] +license = { text = "MIT" } +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", +] +dependencies = ["anyio>=4.5", "click>=8.2.0", "mcp", "psycopg2-binary>=2.9"] + +[project.scripts] +mcp-postgres-knowledge = "mcp_postgres_server.server:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["mcp_postgres_server"] + +[tool.pyright] +include = ["mcp_postgres_server"] +venvPath = "." +venv = ".venv" + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = [] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[dependency-groups] +dev = ["pyright>=1.1.378", "pytest>=8.3.3", "ruff>=0.6.9"] From f0cd082da6598fc863beb95d042a0b22b702f4a4 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:22:01 -0600 Subject: [PATCH 2/9] fix: add .python-version (force-add, matches gitignore exception) and fix import style --- examples/servers/postgres-knowledge-server/.python-version | 1 + .../postgres-knowledge-server/mcp_postgres_server/server.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 examples/servers/postgres-knowledge-server/.python-version diff --git a/examples/servers/postgres-knowledge-server/.python-version b/examples/servers/postgres-knowledge-server/.python-version new file mode 100644 index 000000000..c8cfe3959 --- /dev/null +++ b/examples/servers/postgres-knowledge-server/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index 9073ab927..38cfa9574 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -22,7 +22,8 @@ import psycopg2 import psycopg2.extras from mcp import types -from mcp.server import Server, ServerRequestContext +from mcp.server import Server +from mcp.server import ServerRequestContext # --------------------------------------------------------------------------- # Configuration From b4881c4096a11c70f2a4e17d30247a3cf299c4f2 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:23:54 -0600 Subject: [PATCH 3/9] fix: add language specifier to fenced code block (markdownlint MD040) --- examples/servers/postgres-knowledge-server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/servers/postgres-knowledge-server/README.md b/examples/servers/postgres-knowledge-server/README.md index 7987385f1..88e96ed83 100644 --- a/examples/servers/postgres-knowledge-server/README.md +++ b/examples/servers/postgres-knowledge-server/README.md @@ -10,7 +10,7 @@ Demonstrates: ## Architecture -``` +```text MCP Client (Claude Code, etc.) │ stdio ▼ From e92fdf3340107ee18a30d789182a23915a2b6b52 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:25:38 -0600 Subject: [PATCH 4/9] fix: add blank line before list (markdownlint MD032) --- examples/servers/postgres-knowledge-server/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/servers/postgres-knowledge-server/README.md b/examples/servers/postgres-knowledge-server/README.md index 88e96ed83..5da466a17 100644 --- a/examples/servers/postgres-knowledge-server/README.md +++ b/examples/servers/postgres-knowledge-server/README.md @@ -3,6 +3,7 @@ A production-grade MCP server backed by PostgreSQL with authorization middleware. Demonstrates: + - **Multi-tool MCP server** — knowledge store, task queue, file routing - **Authorization middleware** — filesystem-based identity gate (no ACL database) - **Postgres backend** — Unix socket connection, no host/port exposure From d8681af5a3688e217b20294466692d724c328a2f Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:35:11 -0600 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20ruff=20format=20=E2=80=94=20two=20bl?= =?UTF-8?q?ank=20lines=20before=20module-level=20functions,=20wrap=20long?= =?UTF-8?q?=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgres-knowledge-server/mcp_postgres_server/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index 38cfa9574..089703bdc 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -38,6 +38,7 @@ # Authorization gate # --------------------------------------------------------------------------- + def authorized(app_id: str) -> bool: """Filesystem-based authorization: folder exists → access granted. @@ -55,6 +56,7 @@ def authorized(app_id: str) -> bool: # Postgres helpers # --------------------------------------------------------------------------- + def get_conn(): return psycopg2.connect(dbname=PG_DB, user=PG_USER) @@ -71,7 +73,9 @@ def ensure_schema(conn): ) """) cur.execute("CREATE INDEX IF NOT EXISTS knowledge_app ON knowledge(app_id)") - cur.execute("CREATE INDEX IF NOT EXISTS knowledge_fts ON knowledge USING gin(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')))") + cur.execute( + "CREATE INDEX IF NOT EXISTS knowledge_fts ON knowledge USING gin(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')))" + ) conn.commit() From 72dfef343d2d4434e91dfd1a99dd16c0e0ff688e Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 21:37:40 -0600 Subject: [PATCH 6/9] fix: remove aligned dict spacing (ruff E241) --- .../mcp_postgres_server/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index 089703bdc..01fa60f0b 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -93,9 +93,9 @@ def ensure_schema(conn): "required": ["app_id", "id", "title", "body"], "properties": { "app_id": {"type": "string", "description": "Authorized app identifier"}, - "id": {"type": "string", "description": "Unique record ID"}, - "title": {"type": "string", "description": "Record title"}, - "body": {"type": "string", "description": "Record content"}, + "id": {"type": "string", "description": "Unique record ID"}, + "title": {"type": "string", "description": "Record title"}, + "body": {"type": "string", "description": "Record content"}, }, }, ), @@ -108,7 +108,7 @@ def ensure_schema(conn): "required": ["app_id", "id"], "properties": { "app_id": {"type": "string"}, - "id": {"type": "string"}, + "id": {"type": "string"}, }, }, ), @@ -121,8 +121,8 @@ def ensure_schema(conn): "required": ["app_id", "query"], "properties": { "app_id": {"type": "string"}, - "query": {"type": "string"}, - "limit": {"type": "integer", "default": 10}, + "query": {"type": "string"}, + "limit": {"type": "integer", "default": 10}, }, }, ), From d1105a5257a77afaf1d664b4b7138ee98e58d95e Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 22:12:12 -0600 Subject: [PATCH 7/9] style: ruff format server.py --- .../mcp_postgres_server/server.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index 01fa60f0b..b4fb76406 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -135,9 +135,7 @@ async def handle_list_tools( return types.ListToolsResult(tools=TOOLS) -async def handle_call_tool( - ctx: ServerRequestContext, params: types.CallToolRequestParams -) -> types.CallToolResult: +async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult: args = params.arguments or {} app_id = args.get("app_id", "") @@ -163,8 +161,9 @@ async def handle_call_tool( elif params.name == "knowledge_get": with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - cur.execute("SELECT id, title, body, created FROM knowledge WHERE id=%s AND app_id=%s", - (args["id"], app_id)) + cur.execute( + "SELECT id, title, body, created FROM knowledge WHERE id=%s AND app_id=%s", (args["id"], app_id) + ) row = cur.fetchone() result = dict(row) if row else {"error": "not_found"} @@ -187,9 +186,7 @@ async def handle_call_tool( ) conn.close() - return types.CallToolResult( - content=[types.TextContent(type="text", text=json.dumps(result, default=str))] - ) + return types.CallToolResult(content=[types.TextContent(type="text", text=json.dumps(result, default=str))]) except Exception as exc: return types.CallToolResult( @@ -202,6 +199,7 @@ async def handle_call_tool( # Entry point # --------------------------------------------------------------------------- + @click.command() def main(): """Postgres-backed MCP knowledge server (stdio transport).""" From 4e450556964a9b4d4709510a05892aeafdee3b9d Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 22:21:56 -0600 Subject: [PATCH 8/9] fix: merge imports, wrap long SQL index string for ruff E501 --- .../postgres-knowledge-server/mcp_postgres_server/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index b4fb76406..974a2574b 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -22,8 +22,7 @@ import psycopg2 import psycopg2.extras from mcp import types -from mcp.server import Server -from mcp.server import ServerRequestContext +from mcp.server import Server, ServerRequestContext # --------------------------------------------------------------------------- # Configuration @@ -74,7 +73,8 @@ def ensure_schema(conn): """) cur.execute("CREATE INDEX IF NOT EXISTS knowledge_app ON knowledge(app_id)") cur.execute( - "CREATE INDEX IF NOT EXISTS knowledge_fts ON knowledge USING gin(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')))" + "CREATE INDEX IF NOT EXISTS knowledge_fts ON knowledge" + " USING gin(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')))" ) conn.commit() From d5662c576f1a419d750d491fbbe625721352e581 Mon Sep 17 00:00:00 2001 From: Sean Campbell Date: Wed, 22 Apr 2026 22:48:10 -0600 Subject: [PATCH 9/9] fix: add type annotations, fix isError->is_error for pyright --- .../mcp_postgres_server/server.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py index 974a2574b..647bd3d00 100644 --- a/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py +++ b/examples/servers/postgres-knowledge-server/mcp_postgres_server/server.py @@ -16,6 +16,7 @@ import json import os from pathlib import Path +from typing import Any import anyio import click @@ -60,7 +61,7 @@ def get_conn(): return psycopg2.connect(dbname=PG_DB, user=PG_USER) -def ensure_schema(conn): +def ensure_schema(conn: Any) -> None: with conn.cursor() as cur: cur.execute(""" CREATE TABLE IF NOT EXISTS knowledge ( @@ -142,7 +143,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ if not authorized(app_id): return types.CallToolResult( content=[types.TextContent(type="text", text=f"Unauthorized: no SAFE folder for '{app_id}'")], - isError=True, + is_error=True, ) try: @@ -182,7 +183,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ else: return types.CallToolResult( content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")], - isError=True, + is_error=True, ) conn.close() @@ -191,7 +192,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ except Exception as exc: return types.CallToolResult( content=[types.TextContent(type="text", text=f"Error: {exc}")], - isError=True, + is_error=True, )