-
Notifications
You must be signed in to change notification settings - Fork 324
Add Custom MCP Gateway example + servers (brave, wikipedia, postgres) and CI #133
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
f0b7d54
c17c57e
560fd68
7b95c1b
43c0dda
f93332c
60b8c5d
a282bb3
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,29 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "**" ] | ||
| pull_request: | ||
| branches: [ "**" ] | ||
|
|
||
| jobs: | ||
| lint: | ||
| name: Lint Markdown and YAML | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Markdown Lint | ||
| uses: avto-dev/markdown-lint@v1 | ||
| with: | ||
| config: .markdownlint.yaml | ||
| args: | | ||
| **/*.md | ||
|
|
||
| - name: YAML Lint | ||
| uses: ibiqlik/action-yamllint@v3 | ||
| with: | ||
| file_or_dir: . | ||
| config_file: .yamllint | ||
| strict: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: Deploy Cloudflare Worker (Custom MCP UI) | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: [ "feature/customizations" ] | ||
| paths: | ||
| - 'custom-mcp/cloudflare/**' | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| deployments: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
|
|
||
| - name: Install wrangler | ||
| run: npm i -g wrangler@^3.90.0 | ||
|
|
||
| - name: Deploy | ||
| working-directory: agents/compose-for-agents/custom-mcp/cloudflare | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| run: wrangler deploy --var GATEWAY_URL:${{ secrets.MCP_GATEWAY_URL }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| name: Compose CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "**" ] | ||
| pull_request: | ||
| branches: [ "**" ] | ||
|
|
||
| jobs: | ||
| compose-validate: | ||
| name: Compose config validation (custom-mcp) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Docker version | ||
| run: docker version | ||
| - name: Compose config | ||
| run: | | ||
| docker compose -f custom-mcp/compose.yaml -f custom-mcp/compose.ci.yaml config | ||
| working-directory: agents/compose-for-agents | ||
|
|
||
| e2e-health: | ||
| name: E2E health check (custom-mcp) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Start stack | ||
| run: | | ||
| docker compose -f custom-mcp/compose.yaml -f custom-mcp/compose.ci.yaml up -d | ||
| working-directory: agents/compose-for-agents | ||
| - name: Wait for health | ||
| run: | | ||
| for i in $(seq 1 30); do | ||
| if curl -fsS http://localhost:8811/health; then | ||
| exit 0 | ||
| fi | ||
| sleep 2 | ||
| done | ||
| echo "Gateway health check failed" >&2 | ||
| docker compose -f custom-mcp/compose.yaml -f custom-mcp/compose.ci.yaml logs || true | ||
| exit 1 | ||
| working-directory: agents/compose-for-agents | ||
| - name: Teardown | ||
| if: always() | ||
| run: docker compose -f custom-mcp/compose.yaml -f custom-mcp/compose.ci.yaml down -v --remove-orphans | ||
| working-directory: agents/compose-for-agents |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,5 @@ | |
| /.cursor/ | ||
| **/init-secrets.sh | ||
| **/secret.openai-api-key | ||
| **/.mcp.env | ||
| **/postgres_url | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||
| # Custom MCP Gateway Stack | ||||||
|
|
||||||
| This example wires a Docker MCP Gateway with a flexible set of MCP servers and secrets loaded from a `.mcp.env` file. | ||||||
|
|
||||||
| Prerequisites | ||||||
|
||||||
| - Docker Desktop 4.43+ or Docker Engine with Compose v2.38.1+ | ||||||
| - Optional: Docker Model Runner if you plan to use local models | ||||||
|
|
||||||
| Setup | ||||||
|
||||||
| 1. cd custom-mcp | ||||||
| 2. cp mcp.env.example .mcp.env | ||||||
| 3. Fill in required secrets (e.g., GITHUB_TOKEN for `github-official`). | ||||||
|
|
||||||
| Run | ||||||
|
||||||
| Run | |
| ## Run |
Copilot
AI
Dec 11, 2025
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.
This section header should use a level 2 markdown heading (##) for proper document structure.
| Modify servers | |
| ## Modify servers |
Copilot
AI
Dec 11, 2025
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.
This section header should use a level 2 markdown heading (##) for proper document structure.
| Included servers & secrets | |
| ## Included servers & secrets |
Copilot
AI
Dec 11, 2025
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.
This section header should use a level 2 markdown heading (##) for proper document structure.
| Notes | |
| ## Notes |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| version: "3" | ||
|
|
||
| tasks: | ||
| up: | ||
| desc: Start custom MCP stack | ||
| cmds: | ||
| - docker compose up --build | ||
| dir: . | ||
|
|
||
| down: | ||
| desc: Stop and remove containers | ||
| cmds: | ||
| - docker compose down -v --remove-orphans | ||
| dir: . | ||
|
|
||
| build: | ||
| desc: Build images | ||
| cmds: | ||
| - docker compose build | ||
| dir: . | ||
|
|
||
| clean: | ||
| desc: Clean everything | ||
| cmds: | ||
| - docker compose down -v --remove-orphans | ||
| - docker builder prune -f | ||
| dir: . |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| node_modules/ | ||
| .dist/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Cloudflare Deployment (Wrangler) | ||
|
|
||
| This folder deploys a Cloudflare Worker UI that interacts with your MCP Gateway. It includes bindings for KV, R2, D1, and Durable Objects. You can later connect a Cloudflare Container service for the gateway, or reference a public gateway URL. | ||
|
|
||
| Setup | ||
| 1) Install Wrangler: npm i -g wrangler | ||
| 2) Copy wrangler.toml and set: | ||
| - account_id (or pass via env) | ||
| - vars.GATEWAY_URL (public URL of your MCP Gateway) | ||
| - Fill resource bindings (KV namespace id, R2 bucket name, D1 database info) | ||
| 3) Create resources: | ||
| - KV: wrangler kv namespace create UI_STATE | ||
| - R2: wrangler r2 bucket create <bucket-name> | ||
| - D1: wrangler d1 create MCP_DB | ||
| - DO: first deploy will create migration v1 for SessionDurable | ||
| 4) Preview: wrangler dev | ||
| 5) Deploy: wrangler deploy | ||
|
|
||
| Optional: Cloudflare Containers (Beta) | ||
| - If you run your MCP Gateway on Cloudflare Containers as service "mcp-gateway", add a service binding in wrangler.toml and adjust worker to call that service instead of external GATEWAY_URL. | ||
|
|
||
| Security | ||
| - Do not expose your MCP Gateway publicly without authentication. Consider placing it behind Cloudflare Access and calling from the Worker using service bindings or mTLS. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "custom-mcp-ui-cf", | ||
| "private": true, | ||
| "version": "0.1.0", | ||
| "scripts": { | ||
| "dev": "wrangler dev", | ||
| "deploy": "wrangler deploy" | ||
| }, | ||
| "devDependencies": { | ||
| "wrangler": "^3.90.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| export interface Env { | ||
| GATEWAY_URL: string; | ||
| UI_STATE: KVNamespace; | ||
| ASSETS_BUCKET: R2Bucket; | ||
| MCP_DB: D1Database; | ||
| SESSION_DO: DurableObjectNamespace; | ||
| // MCP_GATEWAY: Service; // if using service binding to container | ||
| } | ||
|
|
||
| export default { | ||
| fetch: async (req: Request, env: Env, ctx: ExecutionContext): Promise<Response> => { | ||
| const url = new URL(req.url); | ||
|
|
||
| // Basic router | ||
| if (url.pathname === "/") { | ||
| return new Response(htmlPage(), { headers: { "content-type": "text/html; charset=utf-8" } }); | ||
| } | ||
|
|
||
| if (url.pathname === "/api/health") { | ||
| try { | ||
| const resp = await fetch(`${env.GATEWAY_URL}/health`, { | ||
| headers: { "accept": "text/plain" }, | ||
| }); | ||
| return new Response(await resp.text(), { status: resp.status, headers: { "content-type": "text/plain" } }); | ||
| } catch (e: any) { | ||
| return Response.json({ error: e?.message ?? String(e) }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| if (url.pathname === "/api/config") { | ||
| const servers = (await env.UI_STATE.get("servers")) ?? "duckduckgo,github-official,brave,wikipedia-mcp,postgres"; | ||
| return Response.json({ gateway: env.GATEWAY_URL, servers: servers.split(",") }); | ||
| } | ||
|
|
||
| if (url.pathname === "/api/session") { | ||
| // Example Durable Object-backed session counter | ||
| const id = env.SESSION_DO.idFromName("global"); | ||
| const stub = env.SESSION_DO.get(id); | ||
| return await stub.fetch(req); | ||
| } | ||
|
|
||
| return new Response("Not found", { status: 404 }); | ||
| } | ||
| }; | ||
|
|
||
| export class SessionDurable { | ||
| state: DurableObjectState; | ||
| env: Env; | ||
| constructor(state: DurableObjectState, env: Env) { | ||
| this.state = state; | ||
| this.env = env; | ||
| } | ||
| async fetch(_req: Request) { | ||
| const key = "counter"; | ||
| let val = Number((await this.state.storage.get(key)) || 0) + 1; | ||
| await this.state.storage.put(key, val); | ||
| return Response.json({ counter: val }); | ||
| } | ||
| } | ||
|
|
||
| function htmlPage(): string { | ||
| return `<!doctype html> | ||
| <html><head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Custom MCP Gateway UI (Cloudflare)</title> | ||
| <style> | ||
| body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 2rem; } | ||
| .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; max-width: 800px; } | ||
| pre { background: #f7f7f7; padding: 0.75rem; border-radius: 6px; overflow: auto; } | ||
| button { padding: 0.5rem 1rem; margin-right: 0.5rem; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Custom MCP Gateway UI (Cloudflare)</h1> | ||
| <div class="card"> | ||
| <p>This Worker lets you verify gateway health and view configured servers via KV.</p> | ||
| <div> | ||
| <button onclick="checkHealth()">Check Health</button> | ||
| <button onclick="loadConfig()">Load Config</button> | ||
| <button onclick="session()">Session Counter (DO)</button> | ||
| </div> | ||
| <h3>Health</h3> | ||
| <pre id="health">(click Check Health)</pre> | ||
| <h3>Config</h3> | ||
| <pre id="config">(click Load Config)</pre> | ||
| <h3>Session</h3> | ||
| <pre id="session">(click Session Counter)</pre> | ||
| </div> | ||
| <script> | ||
| async function checkHealth(){ | ||
| const el = document.getElementById('health'); | ||
| el.textContent = 'Loading...'; | ||
| try { const r = await fetch('/api/health'); el.textContent = await r.text(); } catch(e){ el.textContent = 'Error: '+e.message } | ||
| } | ||
| async function loadConfig(){ | ||
| const el = document.getElementById('config'); | ||
| el.textContent = 'Loading...'; | ||
| try { const r = await fetch('/api/config'); el.textContent = JSON.stringify(await r.json(), null, 2); } catch(e){ el.textContent = 'Error: '+e.message } | ||
| } | ||
| async function session(){ | ||
| const el = document.getElementById('session'); | ||
| el.textContent = 'Loading...'; | ||
| try { const r = await fetch('/api/session'); el.textContent = JSON.stringify(await r.json(), null, 2); } catch(e){ el.textContent = 'Error: '+e.message } | ||
| } | ||
| </script> | ||
| </body></html>`; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| name = "custom-mcp-ui" | ||
| main = "src/worker.ts" | ||
| compatibility_date = "2024-11-05" | ||
| # Uncomment and set your account ID or pass via env | ||
| # account_id = "YOUR_ACCOUNT_ID" | ||
|
|
||
| [vars] | ||
| # Public base URL used by the Worker to reach your MCP Gateway | ||
| # If your gateway is publicly reachable, set e.g. https://gateway.example.com | ||
| # Otherwise, if using Cloudflare Containers service bindings, we will switch to service bindings later. | ||
| GATEWAY_URL = "https://YOUR_GATEWAY_URL" | ||
|
|
||
| [durable_objects] | ||
| bindings = [ | ||
| { name = "SESSION_DO", class_name = "SessionDurable" } | ||
| ] | ||
|
|
||
| [[migrations]] | ||
| tag = "v1" | ||
| new_classes = ["SessionDurable"] | ||
|
|
||
| [[kv_namespaces]] | ||
| binding = "UI_STATE" | ||
| id = "" | ||
| # preview_id = "" # optional | ||
|
|
||
| [[r2_buckets]] | ||
| binding = "ASSETS_BUCKET" | ||
| bucket_name = "" | ||
|
|
||
| [[d1_databases]] | ||
| binding = "MCP_DB" | ||
| database_name = "" | ||
| database_id = "" | ||
|
|
||
| # If using service bindings to a Cloudflare Container named "mcp-gateway", | ||
| # you can uncomment the following once provisioned. | ||
| # [services] | ||
| # bindings = [ | ||
| # { binding = "MCP_GATEWAY", service = "mcp-gateway" } | ||
| # ] |
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 MCPs column lists "duckduckgo, github-official (extensible)" but the actual compose.yaml includes additional servers: brave, wikipedia-mcp, and postgres. Consider updating this to be more accurate, such as "duckduckgo, github-official, brave, wikipedia-mcp, postgres (extensible)" or "duckduckgo, github-official, brave, and more (extensible)".