Skip to content

Repository files navigation

MCP Toolshed

A standalone MCP aggregation layer for Render users. One endpoint, many tools — discovered via search_tools, governed by RBAC, backed by a code registry.

Agents connect to a single URL instead of configuring Render, GitHub, Slack, and custom MCP servers separately.

Deploy to Render

Architecture

  • One Web Service — gateway, search, and all provider adapters run in-process
  • One Postgres — audit log and API key → role mapping (not the tool catalog)
  • Code registry — tool definitions in providers/*.ts, loaded at startup
Agent → POST /mcp → search_tools → get_tool_schema → tools/call → provider adapter

tools/list intentionally returns only meta-tools. Server instructions tell agents to use progressive discovery. The public /mcp endpoint uses MCP Streamable HTTP in stateless JSON-response mode; protocol-level sessions are not persisted between requests.


Setup guide

Prerequisites

  • Node.js 22+
  • Docker (local Postgres only)
  • Render account (dashboard.render.com)
  • Render CLI (optional, for Blueprint deploy): brew install render && render login

1. Clone and configure

git clone https://github.com/render-examples/mcp-toolshed.git
cd mcp-toolshed
cp .env.example .env

Edit .env — at minimum set:

Variable Local value Notes
DATABASE_URL postgresql://toolshed:toolshed@localhost:5433/toolshed Matches docker-compose.yml
TOOLSHED_BOOTSTRAP_API_KEY any secret string Becomes your admin API key after migrate
RENDER_API_KEY your Render API key Enables the Render provider

Generate a production-grade key:

openssl rand -hex 32

2. Run locally

docker compose up -d postgres   # start Postgres on port 5433
npm install
npm run db:migrate              # applies migrations + inserts bootstrap key
npm run dev                     # http://localhost:3000

npm run build compiles the production application to dist/. The Docker image runs that JavaScript directly; tsx is used only by local development and maintenance scripts.

Verify:

curl http://localhost:3000/ready
# → {"status":"ok","toolCount":N}

If you have no provider credentials yet, add to .env:

TOOLSHED_ALLOW_EMPTY=true

3. Deploy to Render

The repo includes a Blueprint (render.yaml) that creates one Web Service + one Postgres database.

Option A — Render CLI

render blueprint launch

Select your workspace and confirm resource creation when prompted.

Option B — Render Dashboard

  1. Go to dashboard.render.comNewBlueprint
  2. Connect the render-examples/mcp-toolshed repository
  3. Review the two resources (mcp-toolshed, toolshed-db) and apply

Secrets to set at deploy time

Render prompts for these (sync: false in render.yaml):

Variable Required Purpose
TOOLSHED_BOOTSTRAP_API_KEY Yes (first deploy) Admin API key; hashed into api_keys on migrate
RENDER_API_KEY Recommended Enables Render MCP tools (render.*)
GITHUB_TOKEN Optional Enables GitHub tools through GitHub's hosted MCP server (github.*)
GITHUB_MCP_URL Optional Defaults to https://api.githubcopilot.com/mcp/
INTEL_MCP_URL With Intelligence URL of the contained intelligence MCP endpoint
INTEL_MCP_API_KEY With Intelligence Service token shared only with intel-mcp
INTEL_MCP_TOOL_PREFIX Optional Match mcp.namespace in intel.yaml; defaults to intel
SLACK_BOT_TOKEN Optional Slack bot token (xoxb-...) — see Slack setup
SLACK_TEAM_ID With Slack Workspace ID (T...) — required with SLACK_BOT_TOKEN
SLACK_CHANNEL_IDS Optional Comma-separated channel IDs to limit access
RENDER_MCP_URL Optional Defaults to https://mcp.render.com/mcp
TICKET_API_URL / TICKET_API_KEY Optional Enables custom inline provider stub
TOOLSHED_AUDIT_RETENTION_DAYS Optional Audit retention; defaults to 30 days
TOOLSHED_AUDIT_MAX_ARGUMENT_BYTES Optional Maximum stored argument payload; defaults to 65536 bytes

Health check note: /ready returns 200 only when Postgres is up and at least one provider loaded tools. For a working deploy, set RENDER_API_KEY (or another provider credential). Use TOOLSHED_ALLOW_EMPTY=true only for dev/testing.

Plan note: Use Starter or higher for the web service. Free tier spins down after inactivity.

Post-deploy checklist

  1. Confirm health: curl https://<your-service>.onrender.com/ready
  2. Unset TOOLSHED_BOOTSTRAP_API_KEY in the Dashboard — auth uses the api_keys table only after first migrate
  3. Connect your MCP client (see below)
  4. Add more API keys via Postgres if needed (see RBAC)

4. Connect an MCP client

Your toolshed URL:

https://<your-service>.onrender.com/mcp

All requests require:

Authorization: Bearer <your-api-key>

Cursor

Add to MCP settings (.cursor/mcp.json or Cursor Settings → MCP):

{
  "mcpServers": {
    "toolshed": {
      "url": "https://<your-service>.onrender.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}

Claude Desktop / other Streamable HTTP clients

Use the same URL and Authorization header. Clients must support MCP Streamable HTTP.

5. Use the agent workflow

After connecting, agents discover tools progressively:

1. tools/list        → [search_tools, get_tool_schema]
2. search_tools      → { "query": "list render services" }
3. get_tool_schema   → { "name": "render.list_services" }
4. tools/call        → { "name": "render.list_services", "arguments": { ... } }

Example search_tools query strings:

  • "list render services"
  • "create pull request on github"
  • "post slack message"

6. Add or enable providers

Providers are TypeScript modules in providers/. Each is enabled when its env vars are set.

Provider File Enable with
Render providers/render.ts RENDER_API_KEY
GitHub providers/github.ts GITHUB_TOKEN (official hosted MCP)
Intelligence providers/intel.ts INTEL_MCP_URL + INTEL_MCP_API_KEY
Slack providers/slack.ts SLACK_BOT_TOKEN + SLACK_TEAM_ID (direct Web API adapter)
Custom providers/custom.ts TICKET_API_URL + TICKET_API_KEY

To add a new provider:

  1. Create providers/my-api.ts (copy a stub)
  2. Register it in providers/index.ts
  3. Commit, push, and redeploy

Slack setup

  1. Create an app at api.slack.com/appsFrom scratch
  2. OAuth & Permissions → add bot scopes: channels:history, channels:read, chat:write, reactions:write, users:read, users.profile:read
  3. Install to Workspace → copy the Bot User OAuth Token (xoxb-...)
  4. Get your Workspace ID (T...) from Slack workspace settings
  5. Set on the Render service (or local .env):
SLACK_BOT_TOKEN=xoxb-...
SLACK_TEAM_ID=T...
  1. Redeploy, then /invite @your-bot in any channel the agent should use

Troubleshooting

Symptom Fix
/ready returns 503 Set a provider credential (RENDER_API_KEY) or TOOLSHED_ALLOW_EMPTY=true
401 on /mcp Check Authorization: Bearer … matches a key in api_keys
Deploy stuck on health check Postgres not ready, or zero providers loaded — check logs
No Render tools in search Verify RENDER_API_KEY is set and service restarted after adding it
No Slack tools in search Set both SLACK_BOT_TOKEN and SLACK_TEAM_ID, then redeploy
Bootstrap key stopped working Expected after unsetting env var — key should still work via api_keys table; re-run migrate if needed

View logs in the Render Dashboard → mcp-toolshedLogs.


Adding a provider

See Setup guide §6 for the workflow. Provider types:

Type Use for
mcp-remote Hosted MCP servers (Render MCP, Intelligence Template)
mcp-stdio Locally installed MCP binaries that require stdio
inline Custom REST APIs with TypeScript handlers

GitHub uses GitHub's maintained hosted MCP endpoint with PAT authentication. Slack uses a small in-process Web API adapter so bot-token deployments do not depend on the archived reference MCP package.

RBAC

Policy lives in config/rbac.ts. Roles: analyst, implementer, deploy-manager, admin.

Upstream tools inherit per-tool risk (list_* / get_* → read; others → provider default).

RBAC is enforced at three layers:

  1. search_tools — only returns discoverable tools (non-empty query required)
  2. get_tool_schema — denied if not allowed to view
  3. tools/call — denied if not allowed to execute

Insert additional API keys (hash must match Node sha256 of the UTF-8 key string):

node -e "const c=require('crypto'); console.log(c.createHash('sha256').update('my-secret-key').digest('hex'))"
INSERT INTO api_keys (key_hash, role, label)
VALUES ('<hash-from-above>', 'implementer', 'ci-bot');

Keys can be time-bounded with expires_at or revoked without deleting audit history:

UPDATE api_keys SET revoked_at = now() WHERE label = 'ci-bot';

Health

  • GET /ready — 200 when Postgres is up, configured providers are healthy, and at least one provider tool is loaded (or TOOLSHED_ALLOW_EMPTY=true)
  • GET /health — same checks plus per-provider and audit-pipeline health, tool counts, timestamps, and last errors

Provider calls are serialized through bounded queues and cancelled at their configured deadline. A failed provider call marks that provider unhealthy; a later successful call restores it.

Audit retention

Arguments are recursively redacted before storage, including nested secret objects and environment-variable key/value pairs. Oversized payloads are truncated. Cleanup runs opportunistically after audit writes (at most hourly) and deletes rows older than TOOLSHED_AUDIT_RETENTION_DAYS.

Project layout

providers/          # Tool sources — edit these
config/rbac.ts      # Role policies
toolshed/           # Core server (don't fork unless extending)
migrations/         # Postgres schema (audit + api_keys)

See seed.md for design research and rationale.

About

MCP Toolshed template. One endpoint, many tools, with permissions, deployed on Render

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages