mcp-resource-server

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.

Category
访问服务器

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 — one SELECT. :name parameters are bound from the tool's arguments by name; a parameter with no argument binds NULL, so LIMIT COALESCE(:limit, 20) and WHERE (:city IS NULL OR city = :city) are the idioms for optional arguments. limit is 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_supported in /.well-known/oauth-protected-resource is 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

  1. Copy verticals/healthcare/ to verticals/<name>/.
  2. Write schema.sql and seed.json for your data.
  3. Write the tools in vertical.json — one SELECT each.
  4. 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, or PINGONE_BASE_URL) — every token is verified against your PingOne environment's keys and rejected on failure. STRICT_AUTH has no effect. Run this way once real tokens are flowing.
  • No JWKS sourceSTRICT_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=true rejects 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.json in 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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选