diff --git a/LIVE-POC.md b/LIVE-POC.md index fb1cb7e..4bac7d2 100644 --- a/LIVE-POC.md +++ b/LIVE-POC.md @@ -48,4 +48,13 @@ Self-hosted on `backstage-wus2-v4` via Flux; vendor Helm chart **1.8.8** 7 main-catalog tools (Slack ×2, GoogleDocs ×4, Brightdata ×1). See `config/targets.yaml`. Confirmed live 2026-06-18: tool list is gateway-wide (same for all `Arcade-User-ID`s). - **Shared reference server:** _name + tools echo/whoami/add (Task 1.4)_ -- **`whoami` identity field:** _exact field the server reads (Task 1.4 / 2.4)_ +- **`whoami` identity field:** server reads `context.user_id` (arcade_mcp_server `Context`), populated by the Engine from the calling user (`Arcade-User-ID` / auth `sub`). + +## Known behaviors (findings) +- **`arcade deploy` is cloud-only.** It validates the server locally fine (health, tool + secret + discovery — our ref server: 3 tools, 0 secrets), but POSTs the deployment to `api.arcade.dev` + (`PROD_ENGINE_HOST`), ignoring the `arcade login --host` coordinator — so against our self-hosted + instance it returns **401**. `deploy` exposes no `--host`. **Implication:** self-hosted custom + servers must be **registered** (run the server + dashboard "Add Server", type Arcade, URL + worker + secret) — the tunnel pattern for local dev, or an in-cluster deploy for prod — not `arcade deploy`. + Relevant to cat-4 (SDK/deploy), cat-8 (deployment), cat-9 (DX). diff --git a/categories/cat1-functional/NOTES.md b/categories/cat1-functional/NOTES.md index 65d5578..3b9cab9 100644 --- a/categories/cat1-functional/NOTES.md +++ b/categories/cat1-functional/NOTES.md @@ -13,6 +13,7 @@ ## Remaining for cat-1 scoring - [ ] 2.2 — connect a **second real MCP client (Claude Code)** to the gateway (no-adapter evidence). - [x] 2.5 — **dynamic registration**: PASS — saved add/remove (−Brightdata, +Youtube) reflected on next list, no restart; draft didn't propagate until Save. -- [ ] 2.7 — **mixed prebuilt + custom**: compose a gateway with a `main` tool + a `lib/mcp_server` tool. (needs reference server → `arcade login`/`arcade deploy`) -- [ ] 2.4 — **`whoami` execution proof** that calls run as the calling user. (needs reference server) +- Reference server built at `lib/mcp_server` (echo/add/whoami); locally validated by `arcade deploy` (3 tools, 0 secrets). **`arcade deploy` is cloud-only (finding)** — see LIVE-POC. +- [ ] 2.7 — **mixed prebuilt + custom**: needs the ref server behind the self-hosted Engine via the **register path** (run `server.py --transport http` + cloudflared tunnel + dashboard Add Server), then compose a gateway (a `main` tool + `echo`). Doubles as cat-9 Stage-2. +- [ ] 2.4 — **`whoami` execution proof**: once registered, call whoami as A vs B (expect A→A, B→B). - [ ] 2.8 — finalize scores once the above land. diff --git a/lib/mcp_server/.env.example b/lib/mcp_server/.env.example new file mode 100644 index 0000000..f091946 --- /dev/null +++ b/lib/mcp_server/.env.example @@ -0,0 +1,13 @@ +# Environment variables for mcp_server MCP server +# +# Copy this file to .env and fill in your values: +# cp .env.example .env +# +# The .env file will be automatically discovered when running your server, +# even from subdirectories like src/mcp_server/. +# +# IMPORTANT: Never commit your .env file to version control! + +# Example secret used by the whisper_secret tool +# Replace with your actual secret value +MY_SECRET_KEY="Your tools can have secrets injected at runtime!" diff --git a/lib/mcp_server/pyproject.toml b/lib/mcp_server/pyproject.toml new file mode 100644 index 0000000..6b3c196 --- /dev/null +++ b/lib/mcp_server/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "mcp_server" +version = "0.1.0" +description = "MCP Server created with Arcade.dev" +requires-python = ">=3.10" +dependencies = [ + "arcade-mcp-server>=1.17.0,<2.0.0", + "httpx>=0.28.0,<1.0.0", +] + +[project.optional-dependencies] +dev = [ + "arcade-mcp[all]>=1.15.0,<2.0.0", + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "mypy>=1.0.0", + "ruff>=0.1.0", +] + +# Tell Arcade.dev that this package has Arcade tools +[project.entry-points.arcade_toolkits] +toolkit_name = "mcp_server" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/mcp_server"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.mypy] +python_version = "3.12" +warn_unused_configs = true +disallow_untyped_defs = false diff --git a/lib/mcp_server/src/mcp_server/__init__.py b/lib/mcp_server/src/mcp_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/mcp_server/src/mcp_server/server.py b/lib/mcp_server/src/mcp_server/server.py new file mode 100644 index 0000000..f98cc81 --- /dev/null +++ b/lib/mcp_server/src/mcp_server/server.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""arcade-eval reference MCP server. + +Deterministic, no-auth tools used as a shared eval fixture across categories: + - echo(text) -> returns text unchanged (protocol / curation / mixed-gateway) + - add(a, b) -> a + b (deterministic invocation) + - whoami() -> the calling user's id, server-side (per-user EXECUTION proof — cat 1/2) + +`whoami` reads `context.user_id`, which the Engine populates from the calling user's identity +(the `Arcade-User-ID` header / auth-server `sub`). If a call as User A returns A and a call as +User B returns B, the tool provably executes as the calling user. +""" +import sys +from typing import Annotated + +from arcade_mcp_server import Context, MCPApp + +app = MCPApp(name="arcade_eval_ref", version="1.0.0") + + +@app.tool +def echo(text: Annotated[str, "Text to echo back"]) -> Annotated[str, "The same text"]: + """Echo the input text unchanged.""" + return text + + +@app.tool +def add( + a: Annotated[int, "First addend"], + b: Annotated[int, "Second addend"], +) -> Annotated[int, "The sum a + b"]: + """Add two integers.""" + return a + b + + +@app.tool +def whoami(context: Context) -> Annotated[str, "The calling user's id as seen server-side"]: + """Return the calling user's identity as the server sees it (proves per-user execution).""" + return context.user_id or "" + + +if __name__ == "__main__": + transport = sys.argv[1] if len(sys.argv) > 1 else "stdio" + app.run(transport=transport, host="127.0.0.1", port=8000)