-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add MCP servers (BROski + Stripe) + Policy Engine foundation (Tracks 1–2) #424
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
Changes from all commits
429dbcc
a426797
bb7f6c6
2c76575
51bb213
dddde55
71da8ef
e29ef99
3624104
22b1014
f725bd5
43910e0
1af87d4
4e5d667
2df8a1b
21c2062
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,38 @@ | ||
| # BROski Economy MCP Server Dockerfile | ||
| # Follows Phase 9 security patterns from HyperCode. | ||
|
|
||
| FROM python:3.11-slim AS base | ||
|
|
||
| # ---------- Part A: OS hardening ---------- | ||
| RUN apt-get update --allow-releaseinfo-change && \ | ||
| apt-get upgrade -y && \ | ||
| apt-get install -y --no-install-recommends \ | ||
| ca-certificates curl libexpat1 openssl && \ | ||
| apt-get clean && \ | ||
| rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* | ||
|
|
||
| # ---------- Part B: pip pinning ---------- | ||
| RUN pip install --upgrade --no-cache-dir \ | ||
| "pip==26.0.1" "setuptools>=80.0.0" "wheel==0.46.2" \ | ||
| "jaraco.context>=6.0.0" "jaraco.functools>=4.1.0" "jaraco.text>=4.0.0" | ||
|
|
||
| # ---------- Runtime stage ---------- | ||
| FROM base AS runtime | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Install dependencies | ||
| COPY requirements.txt . | ||
| RUN pip install --no-cache-dir -r requirements.txt | ||
|
|
||
| # Copy application code | ||
| COPY server.py . | ||
|
|
||
| # Non-root user (Phase 9 pattern) | ||
| RUN groupadd -o -g 999 docker && \ | ||
| useradd --create-home --shell /bin/bash --gid 999 --uid 1000 appuser || true | ||
| USER appuser | ||
|
|
||
| EXPOSE 8099 | ||
|
|
||
| CMD ["python", "server.py"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # BROski Economy MCP Server | ||
|
|
||
| MCP server exposing the BROski$ token economy as tools and resources for AI agents. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### Tools | ||
|
|
||
| - `award_tokens(discord_id: str, amount: int, reason: str)` | ||
| Award BROski$ tokens to a user. Wraps the existing `award_tokens()` SQL function. | ||
|
|
||
| - `spend_tokens(discord_id: str, amount: int, item_slug: str)` | ||
| Spend BROski$ tokens on a shop item or action. Wraps `spend_tokens()`. | ||
|
|
||
| - `get_balance(discord_id: str)` | ||
| Return the current `broski_tokens` balance for a user. | ||
|
|
||
| ### Resources | ||
|
|
||
| - `broski://balance/{discord_id}` | ||
| Read-only resource exposing a user's balance. | ||
|
|
||
| - `broski://transactions/{discord_id}?limit=N` | ||
| Read-only resource exposing recent token transactions. | ||
|
|
||
| ## Architecture | ||
|
|
||
| - Runs on `agent-net` alongside other agents. | ||
| - Connects to the shared PostgreSQL database (async engine). | ||
| - Uses Docker secrets for DB credentials. | ||
| - Exposes an MCP server over stdio / HTTP (depending on deployment). | ||
|
|
||
| ## Deployment | ||
|
|
||
| See `docker-compose.broski-economy-mcp.yml` in the repo root for the service definition. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| fastapi==0.117.0 | ||
| uvicorn[standard]==0.34.0 | ||
| asyncpg==0.30.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| """ | ||
| BROski Economy MCP Server | ||
|
|
||
| Exposes BROski$ token economy as MCP tools and resources. | ||
|
|
||
| Tools: | ||
| - award_tokens(discord_id, amount, reason) | ||
| - spend_tokens(discord_id, amount, item_slug) | ||
| - get_balance(discord_id) | ||
|
|
||
| Resources: | ||
| - broski://balance/{discord_id} | ||
| - broski://transactions/{discord_id}?limit=N | ||
| """ | ||
|
|
||
| import os | ||
| import json | ||
| from typing import Optional | ||
| from contextlib import asynccontextmanager | ||
|
|
||
| import asyncio | ||
| from asyncpg import create_pool, Pool | ||
|
|
||
| # MCP-style primitives (simplified; can be replaced with official MCP SDK later) | ||
| # Tools: callables with JSON Schema descriptions | ||
| # Resources: URI templates + handlers | ||
|
|
||
| DATABASE_URL = os.environ.get("DATABASE_URL") | ||
| if not DATABASE_URL: | ||
| raise RuntimeError("DATABASE_URL env var must be set") | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def get_db_pool(): | ||
| pool: Pool = await create_pool( | ||
| DATABASE_URL, | ||
| min_size=2, | ||
| max_size=10, | ||
| ) | ||
| try: | ||
| yield pool | ||
| finally: | ||
| await pool.close() | ||
|
Comment on lines
+33
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Create one application-scoped database pool per service process. Both MCP servers create and close a new pool for individual tool or resource calls. Under concurrency this can multiply database connections and add avoidable connection churn. Initialise each pool in the FastAPI lifespan, store it in application state, and reuse it for requests. Also applies to 📍 Affects 2 files
🤖 Prompt for AI Agents🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift Require authenticated callers and action authorization before sensitive MCP operations. The BROski routes accept arbitrary 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| # ---------- Tool Implementations ---------- | ||
|
|
||
|
|
||
| async def award_tokens(discord_id: str, amount: int, reason: str) -> dict: | ||
| """ | ||
| Award BROski$ tokens to a user. | ||
| Wraps the existing award_tokens() SQL function (SECURITY DEFINER). | ||
| """ | ||
| async with get_db_pool() as pool: | ||
| async with pool.acquire() as conn: | ||
| row = await conn.fetchrow( | ||
| """ | ||
| SELECT award_tokens($1, $2, $3) AS success; | ||
| """, | ||
| discord_id, | ||
| amount, | ||
| reason, | ||
| ) | ||
| success = bool(row["success"]) | ||
| return {"success": success, "action": "award_tokens", "discord_id": discord_id, "amount": amount, "reason": reason} | ||
|
|
||
|
|
||
| async def spend_tokens(discord_id: str, amount: int, item_slug: str) -> dict: | ||
| """ | ||
| Spend BROski$ tokens on a shop item or action. | ||
| Wraps the existing spend_tokens() SQL function (SECURITY DEFINER). | ||
| """ | ||
| async with get_db_pool() as pool: | ||
| async with pool.acquire() as conn: | ||
| row = await conn.fetchrow( | ||
| """ | ||
| SELECT spend_tokens($1, $2, $3) AS success; | ||
| """, | ||
| discord_id, | ||
| amount, | ||
| item_slug, | ||
| ) | ||
| success = bool(row["success"]) | ||
| return {"success": success, "action": "spend_tokens", "discord_id": discord_id, "amount": amount, "item_slug": item_slug} | ||
|
|
||
|
|
||
| async def get_balance(discord_id: str) -> dict: | ||
| """ | ||
| Return the current broski_tokens balance for a user. | ||
| """ | ||
| async with get_db_pool() as pool: | ||
| async with pool.acquire() as conn: | ||
| row = await conn.fetchrow( | ||
| """ | ||
| SELECT broski_tokens AS balance | ||
| FROM public.users | ||
| WHERE discord_id = $1; | ||
| """, | ||
| discord_id, | ||
| ) | ||
| balance = row["balance"] if row else 0 | ||
| return {"discord_id": discord_id, "balance": balance} | ||
|
|
||
|
|
||
| # ---------- Resource Implementations ---------- | ||
|
|
||
|
|
||
| async def get_balance_resource(discord_id: str) -> dict: | ||
| """ | ||
| Resource handler for broski://balance/{discord_id} | ||
| """ | ||
| return await get_balance(discord_id) | ||
|
|
||
|
|
||
| async def get_transactions_resource(discord_id: str, limit: int = 10) -> list: | ||
| """ | ||
| Resource handler for broski://transactions/{discord_id}?limit=N | ||
| Returns recent token transactions for the user. | ||
| """ | ||
| async with get_db_pool() as pool: | ||
| async with pool.acquire() as conn: | ||
| rows = await conn.fetch( | ||
| """ | ||
| SELECT id, discord_id, amount, balance_after, transaction_type, | ||
| item_slug, reason, created_at | ||
| FROM public.token_transactions | ||
| WHERE discord_id = $1 | ||
| ORDER BY created_at DESC | ||
| LIMIT $2; | ||
| """, | ||
| discord_id, | ||
| limit, | ||
| ) | ||
| return [dict(r) for r in rows] | ||
|
|
||
|
|
||
| # ---------- MCP Server Skeleton ---------- | ||
| # This is a simplified MCP-over-HTTP style server. | ||
| # In a later iteration, this can be replaced/wrapped by the official MCP SDK. | ||
|
|
||
| from fastapi import FastAPI, Request, Response | ||
| import uvicorn | ||
|
|
||
| app = FastAPI(title="BROski Economy MCP Server") | ||
|
|
||
| TOOLS = { | ||
| "award_tokens": { | ||
| "name": "award_tokens", | ||
| "description": "Award BROski$ tokens to a user.", | ||
| "inputSchema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "discord_id": {"type": "string"}, | ||
| "amount": {"type": "integer"}, | ||
| "reason": {"type": "string"}, | ||
| }, | ||
| "required": ["discord_id", "amount", "reason"], | ||
| }, | ||
| }, | ||
| "spend_tokens": { | ||
| "name": "spend_tokens", | ||
| "description": "Spend BROski$ tokens on a shop item or action.", | ||
| "inputSchema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "discord_id": {"type": "string"}, | ||
| "amount": {"type": "integer"}, | ||
| "item_slug": {"type": "string"}, | ||
| }, | ||
| "required": ["discord_id", "amount", "item_slug"], | ||
| }, | ||
| }, | ||
| "get_balance": { | ||
| "name": "get_balance", | ||
| "description": "Return the current broski_tokens balance for a user.", | ||
| "inputSchema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "discord_id": {"type": "string"}, | ||
| }, | ||
| "required": ["discord_id"], | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| RESOURCES = { | ||
| "broski_balance": { | ||
| "uriTemplate": "broski://balance/{discord_id}", | ||
| "name": "broski_balance", | ||
| "description": "Read-only resource exposing a user's BROski$ balance.", | ||
| }, | ||
| "broski_transactions": { | ||
| "uriTemplate": "broski://transactions/{discord_id}?limit=N", | ||
| "name": "broski_transactions", | ||
| "description": "Read-only resource exposing recent token transactions for a user.", | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| @app.get("/health") | ||
| async def health(): | ||
| return {"status": "ok"} | ||
|
|
||
|
|
||
| @app.get("/.well-known/mcp") | ||
| async def mcp_discovery(): | ||
| """ | ||
| MCP discovery endpoint (simplified). | ||
| Returns tools and resources available on this server. | ||
| """ | ||
| return { | ||
| "tools": TOOLS, | ||
| "resources": RESOURCES, | ||
| } | ||
|
|
||
|
|
||
| @app.post("/mcp/tools/{tool_name}") | ||
| async def call_tool(tool_name: str, request: Request): | ||
| """ | ||
| MCP tool invocation endpoint. | ||
| Expects JSON body matching the tool's inputSchema. | ||
| """ | ||
| body = await request.json() | ||
|
|
||
| if tool_name == "award_tokens": | ||
| result = await award_tokens( | ||
| discord_id=body["discord_id"], | ||
| amount=body["amount"], | ||
| reason=body.get("reason", ""), | ||
| ) | ||
| elif tool_name == "spend_tokens": | ||
| result = await spend_tokens( | ||
| discord_id=body["discord_id"], | ||
| amount=body["amount"], | ||
| item_slug=body["item_slug"], | ||
| ) | ||
| elif tool_name == "get_balance": | ||
| result = await get_balance(discord_id=body["discord_id"]) | ||
| else: | ||
| return {"error": f"Unknown tool: {tool_name}"}, 404 | ||
|
|
||
| return {"result": result} | ||
|
|
||
|
|
||
| @app.get("/mcp/resources/broski://balance/{discord_id}") | ||
| async def resource_balance(discord_id: str): | ||
| """ | ||
| MCP resource: broski://balance/{discord_id} | ||
| """ | ||
| result = await get_balance_resource(discord_id) | ||
| return result | ||
|
|
||
|
|
||
| @app.get("/mcp/resources/broski://transactions/{discord_id}") | ||
| async def resource_transactions(discord_id: str, limit: int = 10): | ||
| """ | ||
| MCP resource: broski://transactions/{discord_id}?limit=N | ||
| """ | ||
| result = await get_transactions_resource(discord_id, limit=limit) | ||
| return result | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| uvicorn.run(app, host="0.0.0.0", port=8099) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Stripe MCP Server Dockerfile | ||
| # Follows Phase 9 security patterns from HyperCode. | ||
|
|
||
| FROM python:3.11-slim AS base | ||
|
|
||
| # ---------- Part A: OS hardening ---------- | ||
| RUN apt-get update --allow-releaseinfo-change && \ | ||
| apt-get upgrade -y && \ | ||
| apt-get install -y --no-install-recommends \ | ||
| ca-certificates curl libexpat1 openssl && \ | ||
| apt-get clean && \ | ||
| rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* | ||
|
|
||
| # ---------- Part B: pip pinning ---------- | ||
| RUN pip install --upgrade --no-cache-dir \ | ||
| "pip==26.0.1" "setuptools>=80.0.0" "wheel==0.46.2" \ | ||
| "jaraco.context>=6.0.0" "jaraco.functools>=4.1.0" "jaraco.text>=4.0.0" | ||
|
|
||
| # ---------- Runtime stage ---------- | ||
| FROM base AS runtime | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Install dependencies | ||
| COPY requirements.txt . | ||
| RUN pip install --no-cache-dir -r requirements.txt | ||
|
|
||
| # Copy application code | ||
| COPY server.py . | ||
|
|
||
| # Non-root user (Phase 9 pattern) | ||
| RUN groupadd -o -g 999 docker && \ | ||
| useradd --create-home --shell /bin/bash --gid 999 --uid 1000 appuser || true | ||
| USER appuser | ||
|
|
||
| EXPOSE 8100 | ||
|
|
||
| CMD ["python", "server.py"] |
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: welshDog/HyperCode-V2.4
Length of output: 4341
🏁 Script executed:
Repository: welshDog/HyperCode-V2.4
Length of output: 1553
🏁 Script executed:
Repository: welshDog/HyperCode-V2.4
Length of output: 2184
Upgrade the build-time
pippin.pip==26.0.1is affected by vulnerabilities fixed in26.1and26.1.2. Pinpipto at least26.1.2.🤖 Prompt for AI Agents