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.
- 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.
- Node.js 22+
- Docker (local Postgres only)
- Render account (dashboard.render.com)
- Render CLI (optional, for Blueprint deploy):
brew install render && render login
git clone https://github.com/render-examples/mcp-toolshed.git
cd mcp-toolshed
cp .env.example .envEdit .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 32docker 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:3000npm 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
The repo includes a Blueprint (render.yaml) that creates one Web Service + one Postgres database.
render blueprint launchSelect your workspace and confirm resource creation when prompted.
- Go to dashboard.render.com → New → Blueprint
- Connect the
render-examples/mcp-toolshedrepository - Review the two resources (
mcp-toolshed,toolshed-db) and apply
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.
- Confirm health:
curl https://<your-service>.onrender.com/ready - Unset
TOOLSHED_BOOTSTRAP_API_KEYin the Dashboard — auth uses theapi_keystable only after first migrate - Connect your MCP client (see below)
- Add more API keys via Postgres if needed (see RBAC)
Your toolshed URL:
https://<your-service>.onrender.com/mcp
All requests require:
Authorization: Bearer <your-api-key>
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"
}
}
}
}Use the same URL and Authorization header. Clients must support MCP Streamable HTTP.
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"
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:
- Create
providers/my-api.ts(copy a stub) - Register it in
providers/index.ts - Commit, push, and redeploy
- Create an app at api.slack.com/apps → From scratch
- OAuth & Permissions → add bot scopes:
channels:history,channels:read,chat:write,reactions:write,users:read,users.profile:read - Install to Workspace → copy the Bot User OAuth Token (
xoxb-...) - Get your Workspace ID (
T...) from Slack workspace settings - Set on the Render service (or local
.env):
SLACK_BOT_TOKEN=xoxb-...
SLACK_TEAM_ID=T...
- Redeploy, then
/invite @your-botin any channel the agent should use
| 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-toolshed → Logs.
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.
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:
search_tools— only returns discoverable tools (non-empty query required)get_tool_schema— denied if not allowed to viewtools/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';GET /ready— 200 when Postgres is up, configured providers are healthy, and at least one provider tool is loaded (orTOOLSHED_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.
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.
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.