mcp-resource-server
Serves read-mostly MCP tools across ten mock verticals and investment use cases, each backed by bundled SQLite databases, with optional proxying of investment tools to a user-supplied banking API. It enables clients to query realistic vertical data through OAuth-protected MCP endpoints over HTTP or WebSocket.
README
mcp-resource-server
A banking MCP server whose tools, data, resources and prompts are defined by config files, not code. Switch it to another vertical by changing one setting. Every answer comes from the server's own bundled SQLite database — no backend service is involved. Bring your own PingOne environment for tokens.
Quick start
cp .env.example .env
Edit .env: set MCP_RESOURCE_SERVER_RESOURCE_URI, PINGONE_ENVIRONMENT_ID
and PINGONE_REGION for your own PingOne environment. Leave PINGONE_ISSUER
commented out until you have real tokens (see "Auth modes").
docker compose up --build
The server listens on http://localhost:8081, serving the banking
vertical. Its database is ./data/banking.db, created and seeded at
startup from verticals/banking/; a restart never re-seeds a non-empty
table, so edits you make to the database stick.
Switching vertical
VERTICAL=healthcare docker compose up --build
Now tools/list shows the healthcare tools, scopes_supported is
healthcare:read, and the data comes from ./data/healthcare.db. Nothing
else changes — same auth, same port, same endpoints. VERTICAL names a
folder under verticals/.
What's in a vertical
verticals/banking/
vertical.json # tools, resources, prompts
schema.sql # CREATE TABLE IF NOT EXISTS ...
seed.json # { "<table>": [ rows ] } — applied only to empty tables
A tool is an MCP tool definition plus the SQL that answers it:
{
"name": "get_account_balance",
"description": "Current and available balance for one account.",
"inputSchema": {
"type": "object",
"properties": { "account_id": { "type": "string", "description": "Account id, e.g. ACC-001" } },
"required": ["account_id"]
},
"requiredScopes": ["banking:read"],
"intentHints": ["what's my balance"],
"sql": "SELECT id AS account_id, nickname, balance, available, currency FROM accounts WHERE id = :account_id",
"result": "one"
}
sql— oneSELECT.:nameparameters are bound from the tool's arguments by name; a parameter with no argument bindsNULL, soLIMIT COALESCE(:limit, 20)andWHERE (:city IS NULL OR city = :city)are the idioms for optional arguments.limitis clamped to 1–100.result—"one"returns the row (error if none);"many"returns{ "items": [...], "count": n }.requiredScopes— the bearer token must carry every scope listed.scopes_supportedin/.well-known/oauth-protected-resourceis derived from the catalog, so it always matches.resources[]expose a list tool as an MCP resource;prompts[]are user-message templates with{{argument}}placeholders.
The server validates the folder at startup and refuses to start on a bad
one, naming the file and tool: SQL that isn't a SELECT, SQL that doesn't
prepare against schema.sql, a required argument with no :param, a
duplicate name, a seed table the schema doesn't create.
Adding a vertical
- Copy
verticals/healthcare/toverticals/<name>/. - Write
schema.sqlandseed.jsonfor your data. - Write the tools in
vertical.json— oneSELECTeach. VERTICAL=<name> docker compose up --build.
npm test runs every tool of every folder under verticals/ against its
own seed and fails on one that returns nothing.
Kubernetes (Helm)
A chart lives in helm/mcp-resource-server/. The image is published by
GitHub Actions to ghcr.io/curtismu7/mcp-resource-server on every push to
main (latest, sha-<short>) and on v* tags.
helm install mcp helm/mcp-resource-server \
--set ingress.enabled=true --set ingress.host=mcp.example.com \
--set pingone.environmentId=<env-id> --set pingone.region=com
resourceUri (the audience inbound tokens must carry) defaults to
https://<ingress.host>; set it explicitly when you run without an ingress.
Set pingone.issuer (https://auth.pingone.<region>/<env-id>/as) to verify
token signatures; until then strictAuth decides whether unverifiable
tokens are accepted (see "Auth modes"). No Secrets are needed: the server
only verifies tokens. Ingress defaults are for an nginx controller
(className: nginx-public, buffering off, long read timeout — MCP streams
hold responses open).
Switch vertical without a new image:
helm upgrade mcp helm/mcp-resource-server --reuse-values --set vertical=healthcare
Add a vertical without a new image — the three files go in values and are mounted next to the built-in ones:
verticals:
retail:
vertical.json: |
{ "name": "retail", "resourceName": "Retail MCP Server", "tools": [ ... ] }
schema.sql: |
CREATE TABLE IF NOT EXISTS orders ( ... );
seed.json: |
{ "orders": [ ... ] }
helm upgrade mcp helm/mcp-resource-server --reuse-values -f retail-values.yaml --set vertical=retail
The database is an emptyDir seeded at startup; set persistence.enabled=true
for a PVC if edits made to the database must survive a restart. While the
GHCR package is private, create a pull secret and set imagePullSecrets.
npm test renders the chart with helm template and checks the manifests
when helm is installed.
Banking tools (default)
| Tool | Arguments |
|---|---|
list_accounts |
— |
get_account |
account_id |
get_account_balance |
account_id |
list_transactions |
account_id, limit? |
search_transactions |
query, limit? |
list_cards |
— |
get_statement |
account_id, period (YYYY-MM) |
find_branches |
city?, zip? |
Seeded accounts: ACC-001 checking, ACC-002 savings, ACC-003 money
market, ACC-004 credit card; statements for 2026-06 and 2026-07.
Verify it's running
curl -s http://localhost:8081/health
curl -s http://localhost:8081/.well-known/oauth-protected-resource
Auth modes
Whether a token's signature is verified is decided by whether a JWKS source
is configured — not by STRICT_AUTH:
- JWKS source set (
PINGONE_ISSUER,PINGONE_JWKS_URI, orPINGONE_BASE_URL) — every token is verified against your PingOne environment's keys and rejected on failure.STRICT_AUTHhas no effect. Run this way once real tokens are flowing. - No JWKS source —
STRICT_AUTH=false(the shipped default) accepts a well-formed token with a console warning, so you can exercise every tool with a hand-made token before PingOne is wired up;STRICT_AUTH=truerejects every token instead. Do not leave the default reachable by more than you.
.env.example ships with all three JWKS variables commented out for that
reason — uncomment one when you have real tokens.
Getting a bearer token
Every tool call needs a bearer token whose aud claim matches
MCP_RESOURCE_SERVER_RESOURCE_URI and whose scope claim covers the tool
you're calling (see tools/list for the authoritative, current list — it's
generated from this server's own registry).
Local testing (STRICT_AUTH=false) — any well-formed JWT with the right
claims works; the signature isn't checked.
node -e "
const b64 = s => Buffer.from(JSON.stringify(s)).toString('base64url');
const header = b64({alg:'none',typ:'JWT'});
const payload = b64({sub:'test-user',scope:'banking:read',aud:'your-resource-uri',exp:Math.floor(Date.now()/1000)+3600});
console.log(header+'.'+payload+'.');
"
(swap aud for your own MCP_RESOURCE_SERVER_RESOURCE_URI value, and
scope for whatever tool(s) you're testing)
Real tokens (STRICT_AUTH=true) — mint one from your PingOne
environment. A client-credentials grant against your PingOne token endpoint,
requesting this server's audience as the resource:
curl -s -X POST "https://auth.pingone.<region>/<env-id>/as/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=<your PingOne worker app client id>" \
-d "client_secret=<your PingOne worker app client secret>" \
-d "scope=<space-separated scopes, e.g. banking:read>" \
-d "resource=<MCP_RESOURCE_SERVER_RESOURCE_URI value>"
This requires that client's app to be authorized for this resource and those scopes in PingOne (Applications → your app → Resources) — a PingOne-side setup step this server doesn't do for you.
Calling a tool directly (sanity check)
TOKEN="<paste a token from above>"
curl -s -X POST http://localhost:8081/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_accounts","arguments":{}}}'
Connecting an MCP client
The server speaks MCP over both WebSocket and HTTP (streamable, POST /mcp)
on the same port — ws://localhost:8081 or http://localhost:8081/mcp.
MCP Inspector (the official dev tool — works with any server and lets you set a manual header, so it's the most reliable way to test this one):
npx @modelcontextprotocol/inspector
Set Transport to "Streamable HTTP", URL to http://localhost:8081/mcp, and
add an Authorization: Bearer <token> header in the Inspector's connection
settings before connecting.
Claude Desktop / Cursor / Windsurf (static config, HTTP transport):
{
"mcpServers": {
"mcp-resource-server": {
"url": "http://localhost:8081/mcp",
"transport": "http"
}
}
}
- Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.json - Cursor:
.cursor/mcp.jsonin your project root - Windsurf:
~/.codeium/windsurf/mcp_config.json
These configs have no field for a static bearer token — when the client
calls a protected tool it reads /.well-known/oauth-protected-resource,
finds your PingOne environment as the authorization server, and prompts you
to sign in. That only works once your PingOne environment has an OAuth
client registered for that specific MCP client app, using the redirect URI
that client's own docs specify — a PingOne-side setup step outside this
server. Restart the client after editing its config.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.