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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
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
32 changes: 32 additions & 0 deletions .github/workflows/cloudflare-deploy.yml
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 }}
46 changes: 46 additions & 0 deletions .github/workflows/compose-ci.yml
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
/.cursor/
**/init-secrets.sh
**/secret.openai-api-key
**/.mcp.env
**/postgres_url
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The demos support using OpenAI models instead of running models locally with Doc
| [ADK](https://github.com/google/adk-python) Sock Store Agent | Multi-Agent | qwen3 | MongoDb, Brave, Curl, | [./adk-sock-shop](./adk-sock-shop/) | [compose.yaml](./adk-sock-shop/compose.yaml) |
| [Langchaingo](https://github.com/tmc/langchaingo) DuckDuckGo Search | Single Agent | gemma3 | duckduckgo | [./langchaingo](./langchaingo) | [compose.yaml](./langchaingo/compose.yaml) |
| [MinionS](https://github.com/HazyResearch/minions) Cost-Efficient Local-Remote Collaboration | Local-Remote Protocol | qwen3(local), gpt-4o(remote) | | [./minions](./minions) | [docker-compose.minions.yml](https://github.com/HazyResearch/minions/blob/main/apps/minions-docker/docker-compose.minions.yml) |
| Custom MCP Gateway (this repo) | Gateway + Tools | none | duckduckgo, github-official (extensible) | [./custom-mcp](./custom-mcp) | [compose.yaml](./custom-mcp/compose.yaml) |
Copy link

Copilot AI Dec 11, 2025

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)".

Suggested change
| Custom MCP Gateway (this repo) | Gateway + Tools | none | duckduckgo, github-official (extensible) | [./custom-mcp](./custom-mcp) | [compose.yaml](./custom-mcp/compose.yaml) |
| Custom MCP Gateway (this repo) | Gateway + Tools | none | duckduckgo, github-official, brave, wikipedia-mcp, postgres (extensible) | [./custom-mcp](./custom-mcp) | [compose.yaml](./custom-mcp/compose.yaml) |

Copilot uses AI. Check for mistakes.

## License

Expand Down
48 changes: 48 additions & 0 deletions custom-mcp/README.md
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
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The section headers in this README file should use proper markdown heading levels. "Prerequisites", "Setup", "Run", "Modify servers", "Included servers & secrets", and "Notes" should be marked as level 2 headings (##) for better document structure and accessibility.

Copilot uses AI. Check for mistakes.
- Docker Desktop 4.43+ or Docker Engine with Compose v2.38.1+
- Optional: Docker Model Runner if you plan to use local models

Setup
Copy link

Copilot AI Dec 11, 2025

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.

Copilot uses AI. Check for mistakes.
1. cd custom-mcp
2. cp mcp.env.example .mcp.env
3. Fill in required secrets (e.g., GITHUB_TOKEN for `github-official`).

Run
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
Run
## Run

Copilot uses AI. Check for mistakes.
- Start the stack:
docker compose up --build

- The gateway will listen on port 8811. A health endpoint is available at:
http://localhost:8811/health

Modify servers
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
Modify servers
## Modify servers

Copilot uses AI. Check for mistakes.
- Edit compose.yaml and add or remove `--servers=...` entries on the mcp-gateway service.
- If a server needs credentials, add them to `.mcp.env` and reference via `--secrets=...` if required.

Included servers & secrets
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
Included servers & secrets
## Included servers & secrets

Copilot uses AI. Check for mistakes.
- duckduckgo: No secret required (optional app name via DUCKDUCKGO_APP_NAME)
- github-official: Requires GITHUB_TOKEN in .mcp.env
- brave: Requires BRAVE_API_KEY in .mcp.env
- wikipedia-mcp: No secret required
- postgres with SQL query tool:
- Create a file named `postgres_url` in this directory containing the DSN, e.g.
postgres://user:password@host:5432/dbname
- This is mounted as a secret named `database-url` and used by the gateway.

Notes
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
Notes
## Notes

Copilot uses AI. Check for mistakes.
- The example includes a minimal curl-based `mcp-client` container that checks gateway health.
- To integrate with an agent, set its MCP server URL to the gateway endpoint, e.g. `http://mcp-gateway:8811/sse` for SSE transport.

Agent integration patterns
- LangGraph/LangChain: set MCP endpoint env (e.g., MCP_SERVER_URL=http://mcp-gateway:8811/sse) in your agent container.
- Agno/ADK: point to MCP gateway URL (SSE or streaming) as shown in existing examples in this repo.
- UI frontends (e.g., Vercel AI SDK demo): configure your backend/agent server to call the gateway; avoid exposing gateway publicly.

Troubleshooting
- 401/403 from servers: ensure tokens (e.g., GITHUB_TOKEN, BRAVE_API_KEY) are present in .mcp.env and mapped via --secrets if required.
- Postgres errors: verify postgres_url content is a valid DSN and the DB is reachable from the gateway container.
- Port already in use: change ports mapping under mcp-gateway, e.g., "8812:8811", and curl http://localhost:8812/health.
- CI differences: CI uses compose.ci.yaml override with only duckduckgo and wikipedia (no secrets, no Docker API socket).
27 changes: 27 additions & 0 deletions custom-mcp/Taskfile.yaml
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: .
2 changes: 2 additions & 0 deletions custom-mcp/cloudflare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.dist/
23 changes: 23 additions & 0 deletions custom-mcp/cloudflare/README.md
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.
12 changes: 12 additions & 0 deletions custom-mcp/cloudflare/package.json
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"
}
}
108 changes: 108 additions & 0 deletions custom-mcp/cloudflare/src/worker.ts
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>`;
}
41 changes: 41 additions & 0 deletions custom-mcp/cloudflare/wrangler.toml
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" }
# ]
Loading