mcp-scan-demo
Allows users to register and log in via GitHub OAuth, and enables tools such as search_repos that search GitHub repositories using the user's GitHub access token.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-scan-demoShow the latest 5 CFX transfers for cfx:aanjcf1esdz50j6zhkm0k60wc7669tfkw28mzudg24."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Demo 服务(TypeScript)
一个开箱即跑的 MCP(Model Context Protocol)服务端示例:
使用官方
@modelcontextprotocol/sdk,基于 Streamable HTTP 传输(不自己造轮子)内置认证管理模块:JWT 签发/校验、Bearer 鉴权、账号密码注册 / 登录、一键注册
实现 MCP 标准授权发现(OAuth 2.1 Discovery):RFC 9728 受保护资源元数据 + RFC 8414 授权服务器元数据 + RFC 7591 动态客户端注册 + RFC 7636 PKCE + RFC 8707
resource, Codex / Claude Desktop / MCP Inspector 等客户端可自行发现并完成登录接入 GitHub OAuth 2.0(用
simple-oauth2,可在 Admin 管理端在线配置,既可注册也可登录)Admin 管理端(带页面):用户列表/详情、GitHub 参数设置、签发令牌、调用统计总览
用户 Profile 页面:查看自己的资料与个人维度调用统计
MCP 调用统计:按用户维度 + 总计(请求数 / 工具调用 / 按天 / 最近调用)
用 Codex 跑集成测试,测试会把可访问 URL 打印出来供用户打开
目录结构
src/
config.ts 运行时配置(端口 / JWT 密钥 / 实例 ID / admin 令牌)
db.ts 存储层(Sequelize ORM:自动建表 + 防抖写库 + 旧 JSON 自动导入;
默认 SQLite,可切 MySQL——见下方「存储后端」)
settings.ts GitHub OAuth 运行时可变设置(落盘)
stats.ts MCP 调用统计(计数器 + 环形缓冲 + 落盘)
auth/
store.ts 用户存储(内存 + 落盘 + 按需落库 + 列表/详情)
context.ts 请求级鉴权上下文(AsyncLocalStorage)
manager.ts 注册 / 签发 / 校验 JWT / 请求鉴权
github.ts GitHub OAuth 2.0 接入(凭据每次现读,state 为自签名 JWT)
mcp/
server.ts MCP 服务端:server_info / login / whoami / register_user / my_stats / search_repos
oauth/
metadata.ts RFC 9728 / RFC 8414 元数据文档 + 401 头取值(改前先读文件头注释)
tickets.ts 自包含加密票据(HKDF 派生密钥 + AES-256-GCM + AAD 绑定用途)
clients.ts RFC 7591 动态客户端注册(client_id 即票据,回环地址忽略端口)
register.ts POST /oauth/register —— DCR 端点
authorize.ts GET/POST /oauth/authorize —— 登录页与授权码签发
token.ts POST /oauth/token —— authorization_code(PKCE S256)/ refresh_token
web/
app.ts Express 入口:门户 / 注册 / GitHub OAuth / /mcp / 挂载 admin 与 profile
http.ts 请求工具:基础地址推导、令牌提取、async 包装、Cookie 读取
layout.ts 共享页面布局 + HTML 转义 esc()
admin.ts Admin 管理端(路由 + 页面)
profile.ts 用户 Profile 页面
index.ts 启动入口(顶部加载 .env)+ 连库预热 + 打印访问 URL 横幅 + 退出前刷库
test/
integration.test.ts 集成测试:健康检查 / 注册 / 握手 / 登录引导 / 405 / XSS / my_stats
admin.test.ts Admin:登录鉴权 / 用户管理 / GitHub 设置 / 统计接口
stats.test.ts 调用统计增量断言 / 按需落库 / Profile 页面
admin-token-guard.test.ts 公网默认令牌防护(503)
setup-page.test.ts 免登录接入页 /setup:无令牌泄露、配置可复制、MCP 工具返回 setupUrl
dotenv.test.ts .env 自动加载(入口 import 顺序 + 平台 env 优先于 .env)
codex.json / AGENTS.md Codex 配置与代理指示Related MCP server: XRPL Data MCP
配置(环境变量 / .env)
服务启动时会自动读取项目根目录的 .env(dotenv 在入口 src/index.ts 顶部加载):
cp .env.example .env # 按需修改;.env 已在 .gitignore 中,不会被提交
npm start # 无需再手动 export / 写前缀两条必须记住的语义:
.env不覆盖已存在的环境变量(dotenv默认override: false)。 所以部署平台注入的JWT_SECRET/ADMIN_TOKEN/PUBLIC_BASE_URL永远优先于仓库里的.env, 线上配置不会因为仓库里多了个.env而被悄悄改掉。本地想临时压过.env,照旧用前缀即可:PORT=4001 npm start。.env只影响本地开发。公网部署用的是平台注入的环境变量,跟文件无关。
实现上
src/config.ts在模块顶层就把 env 快照成常量,因此import 'dotenv/config'必须是src/index.ts的第一条 import——重排会让.env静默失效。test/dotenv.test.ts锁住了这个顺序。
快速开始
npm install
npm start # 启动后控制台会打印可访问 URL,含 /admin 与 /profile打开门户地址(默认 http://localhost:3000/):
接入 MCP 客户端(免登录,可直接转发)
新用户不需要先注册就能连接:支持标准 OAuth Discovery 的客户端会在第一次请求时被 自动引导登录,无需任何手工配置。
所以准备了一个免登录、不含任何令牌的接入页:https://你的域名/setup
页面直接给出可一键复制的
mcpServers配置(URL 里没有 token)三步引导:复制配置 → 客户端自动弹登录页 → 不支持的客户端手工填令牌
附
curl冒烟命令与常见坑(405 是正常的、别用Authorization头、401 是登录入口不是故障)首页
/的「MCP 客户端配置」卡片里也放了一份同样的配置块
这个链接可以放心发给任何人——页面里没有任何令牌。测试
test/setup-page.test.ts专门断言了页面与首页不出现mcp_demo_开头的真实令牌。
用户侧:注册与登录(三种方式)
方式 | 入口 | 说明 |
账号密码注册 |
| 用户名(3~32 位,仅字母/数字/ |
账号密码登录 |
| 成功后直接返回 Bearer 令牌 |
GitHub 注册 / 登录 |
| 首次即注册,之后每次登录复用同一账号( |
一键注册(Agent 用) |
| 直接发令牌、无密码,供钉钉等 AI Agent 自动注册 |
网页注册/登录均为
POST表单,配合SameSite=LaxCookie + Origin 校验防 CSRF;口令只经表单提交,绝不以 URL /GET/Authorization传递。
登录态自动保持:注册 / 登录(账号密码、GitHub、钱包)成功后会下发会话 Cookie
mcp_demo_user(HttpOnly + SameSite=Lax + 7 天),之后直接访问/profile、/recharge就是登录态,不必再往 URL 上拼?token=(令牌也不该进浏览器历史)。/mcp端点刻意不读这份 Cookie —— 否则第三方页面能带着浏览器登录态调受保护工具(CSRF), MCP 客户端必须显式带令牌。退出登录走/logout。注意:部署平台网关会把响应里的
SameSite=Lax改写成SameSite=None(跨站也会带 Cookie), 所以 CSRF 防护不靠 SameSite,而是靠「/mcp不读 Cookie」+「所有写路由校验 Origin」两道。
在
/register(或/auth/github)完成注册 → 拿到 Bearer 令牌(浏览器已自动登录,页面上有「查看我的 Profile →」入口)把令牌拼到端点 URL 后,或放到请求头
X-Authorization:
{
"mcpServers": {
"mcp-demo": {
"url": "http://localhost:3000/mcp?token=<你的令牌>",
"transport": "streamable-http"
}
}
}Admin 管理端
登录地址
/admin,令牌来自环境变量ADMIN_TOKEN(本地不设置时用默认dev-admin-change-me)。功能:用户列表(搜索 / 详情 / 签发新令牌 / 删除)、GitHub OAuth 参数在线设置、 调用统计总览(总计卡片 + 工具 Top + 调用方 Top + 最近 14 天 + 最近调用)。
用户 Profile
登录后访问
/profile查看自己的资料、按工具/按天的个人统计,并复制 MCP 客户端配置(登录态由会话 Cookie 保持,无需拼?token=)。充值页会在转账框下方显示该钱包在收款资产上的可用余额(ERC20 走
balanceOf,原生币走eth_getBalance,都用服务端配置的 RPC 读)。余额不足会被拦下;读不到则只提示、不阻拦—— RPC 抖一下不该让用户充不了值。充值页在钱包转账前会比对
eth_chainId:不在收款链上就自动唤起切换网络, 钱包里没这条链时请求添加(RPC 用充值设置里配的地址)。链 ID 由保存配置时从 RPC 自动识别, 手填与 RPC 不一致时以 RPC 为准——填错链等于让用户把钱转到另一条链上。
调用统计
总计与用户维度在 Admin 仪表盘查看;个人维度可在 MCP 客户端调用
my_stats工具,或在/profile页查看。
GitHub OAuth(可选,可在线配置)
在 https://github.com/settings/developers 新建 OAuth App,回调填
<base>/auth/github/callback二选一配置凭据:
在
.env(或环境变量)填GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET(启动时读入)或登录 Admin 管理端 → GitHub 设置在线填写(立即生效,无需重启)
访问
/auth/github完成登录,登录后返回的令牌即可调用search_repos
标准 OAuth 2.1 授权发现(Authorization Discovery)
不带令牌请求 /mcp 会拿到 401 + WWW-Authenticate,客户端据此自行完成整条登录流程:
客户端 ──POST /mcp──▶ 401 WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp"
──GET 该地址──▶ { resource, authorization_servers: ["https://host"] } (RFC 9728)
──GET /.well-known/oauth-authorization-server──▶ { authorization_endpoint, token_endpoint,
registration_endpoint, … } (RFC 8414)
──POST /oauth/register──▶ { client_id } (RFC 7591 DCR)
──浏览器导航 /oauth/authorize?code_challenge=…&resource=…──▶ 登录页 (RFC 7636 PKCE)
──POST /oauth/token(code + code_verifier + resource)──▶ access_token (RFC 8707)
──POST /mcp(Authorization: Bearer …)──▶ 200端点 | 说明 |
| 受保护资源元数据(canonical 路径版本,主用) |
| 同上,给把 canonical 当成纯 origin 的客户端兜底 |
| 授权服务器元数据; |
| 动态客户端注册,免鉴权,只发公开客户端( |
| 登录页 + 授权码签发(授权码 60 秒有效) |
|
|
几条实现上的取舍,改代码前值得知道:
元数据地址必须带
/mcp后缀。canonical URI 是https://host/mcp,按 RFC 9728 §3.1 well-known 要插在 host 与 path 之间;只挂根路径会让客户端 404 后放弃发现。401 头里只给
resource_metadata,授权服务器地址由客户端从 PRM 派生。AS 元数据里刻意不声明
client_id_metadata_document_supported—— 官方 SDK 一见到它 就走 CIMD,绕过我们的 DCR 端点。受众(
aud)不符返回 403 而不是 401。官方 SDK 有熔断:鉴权流程完成后再收到 401 会 直接抛错而不是重跑流程,401 会把「受众不符」伪装成「还没登录」,表现为死循环。所有 OAuth 状态都是自包含加密票据(
src/oauth/tickets.ts),不落盘。persist.ts无锁无 CAS(并发写会互相覆盖)、settings.json每个进程只读一次, 所以必须用「签名的、自带内容的」票据。代价见「已知限制」。
想关掉强制登录(恢复旧的匿名握手,例如给钉钉这类不带 OAuth 的客户端用):
MCP_DEMO_REQUIRE_AUTH=off npm start # 默认 on验证是否真的对第三方客户端可用(用官方 SDK 的 auth() 跑完整流程,不需要人工点浏览器):
npm run oauth:e2e⚠️ 公网部署必须显式注入
PUBLIC_BASE_URL。上面所有地址(canonical resource、 元数据 URL、令牌的aud)都由它推导。部署平台网关只转发内部 host, 缺了这个变量会推导出内网地址 —— 表现为客户端发现到的资源是内网域名、令牌aud对不上而被 403。
部署到公网(钉钉等远程客户端测试)
本项目已发布为在线应用,公网地址:
https://a8b79d8a477856f1e.app.workbuddy.link部署适配:平台网关会改写
Authorization头。本服务已做:
鉴权无状态化:JWT 内嵌用户声明,校验不依赖服务端内存存储,重启后依然有效。
MCP 传输无状态化:每次请求新建独立 transport(关闭会话管理),不依赖进程内会话。
鉴权通道避开
Authorization:服务端只认X-Authorization头或?token=参数 (必须带本服务前缀mcp_demo_),网关注入的Authorization一律忽略。支持匿名握手 + 登录引导:未携带令牌时不会拒绝连接,而是暴露
login工具、 受保护工具返回中文登录引导(含可点击的登录 URL)。存储后端可切换:默认所有数据集落在
data/mcp-demo.sqlite(users / billing / recharge_records / recharge_settings / github_settings / stats 六张表),启动自动建表、旧 JSON 自动导入。 也可切换为 MySQL(见下方「存储后端」)。存储不存在按进程 / 实例拆分——见下方单实例说明。
存储后端(SQLite / MySQL)
存储层由 src/db.ts 实现,通过 STORAGE_DRIVER 选择方言(默认 sqlite):
变量 | 说明 | 默认 |
|
|
|
| MySQL 连接串(完整 | 空 |
| 分项连接参数 |
|
| 账号 / 口令(口令原样传入,不做 trim) |
|
| 数据库名 |
|
/health 与 Admin 控制台会回显当前 db 方言与连接描述(不含口令)。
重要限制(务必先读):
单实例持久化:MySQL 后端沿用「内存态为唯一权威 + 写库为整表替换(事务保护)」的模型, 与 SQLite 一致。多副本 / 多进程共用同一 MySQL 库会导致跨实例数据覆盖——整表替换会清掉其它实例 刚写入的行、再写入本进程的内存快照。因此 MySQL 仅适合单实例部署,请勿多副本共享同一数据库。
SQLite→MySQL 不自动迁移:若
DATA_DIR下已存在mcp-demo.sqlite(说明之前跑在 SQLite 上、旧 JSON 已迁走), 直接设STORAGE_DRIVER=mysql会启动失败(fail-fast),避免静默以空库启动丢失数据。请先手动把 SQLite 数据迁到 MySQL,或确认不再需要旧数据后删除该.sqlite文件再启动。MySQL 启动时会把DATA_DIR下尚未导入的旧 JSON 按表为空才导入:真正导入过的数据集,其文件移入
DATA_DIR/_migrated_json/;表非空而被跳过的文件(可能含额外用户/配置),移入
DATA_DIR/_migrated_json/_skipped_for_review/(待人工处理,不会留在DATA_DIR),并写_migrated_json/_status.json记录每个数据集的imported/skipped标记,后续启动会忽略已处理的数据集,避免清空某表后重启又重放陈旧数据。
已有 MySQL 表(早期
TEXT列)如需更大的统计容量,请将 stats 相关列改为LONGTEXT(DataTypes.TEXT('long'))。
钉钉 MCP 配置(不含 token,靠登录引导)
配置见 mcp-config-dingtalk.json,只有干净的端点 URL,不含任何令牌:
{
"mcpServers": {
"mcp-demo": {
"url": "https://a8b79d8a477856f1e.app.workbuddy.link/mcp",
"transport": "streamable-http"
}
}
}接入后在钉钉里无需任何令牌即可连接,服务以「匿名」身份握手并提供 login 工具。
完成登录的流程如下:
在钉钉对话里问「怎么登录 / 你是谁」,AI 会调用
login或whoami工具, 返回一键注册链接(…/register)和 GitHub 登录链接(/auth/github)。用户在浏览器打开注册链接 → 一键拿到令牌(形如
mcp_demo_xxx)。把令牌填回钉钉的 MCP 配置「鉴权 / 自定义请求头」:
推荐用
X-Authorization: Bearer <token>(不要用Authorization,会被平台网关改写吞掉);或在端点 URL 后追加
?token=<token>。
之后钉钉即可调用
whoami/search_repos/my_stats等受保护工具。
要点:
令牌有效期 7 天;无状态 JWT,重新部署后旧令牌仍有效。
若钉钉只提供「SSE」类型选项,选它即可(底层仍 POST 到该 URL)。
部署时用
--start-cmd "PUBLIC_BASE_URL=... ADMIN_TOKEN=... npm start"注入对外域名与 admin 令牌; 不注入ADMIN_TOKEN时,公网地址会被判定为非本地环境,Admin 管理端返回 503 禁用。
测试
本仓库已配好 Codex:
npm install -g @openai/codex # 安装 Codex CLI(需配置 CODEX_API_KEY)
npm run test:codex # 等价于 codex exec "...运行 npm test 并回报 URL..."或直接本地运行(无需 Codex):
npm test测试会启动服务,把可访问 URL 打印到 stdout,并断言:健康检查可用、一键注册返回令牌、
带令牌能调用 whoami、Admin 鉴权与用户管理、GitHub 设置即时生效、
调用统计计数正确(通知类不计入、工具调用单列)、Profile 页面转义安全等。
与授权相关的三份测试单独说:
文件 | 覆盖 |
| 发现契约:元数据地址(含 |
| 扮演真实客户端跑完 DCR → 授权 → 换令牌 → 调 MCP → 刷新,重点覆盖失败路径(重放 / PKCE 错误 / 篡改 / 过期 / 受众不符) |
| 用官方 SDK 的 |
暴露的 MCP 工具
工具 | 说明 | 是否需要鉴权 |
| 返回服务元信息 + 登录入口 | 否(匿名可用) |
| 返回一键注册页与 GitHub OAuth 登录链接(引导登录) | 否(匿名可用) |
| 返回当前登录用户;未登录时返回登录引导 | 建议(匿名返回引导) |
| 一键注册新用户并返回令牌 | 否(匿名可用) |
| 查看当前用户自己的调用统计(工具/按天) | 是(需登录) |
| 用当前用户 GitHub 令牌搜索仓库(演示 OAuth) | 是 + 需 GitHub 登录 |
| 列出某 Conflux Core 账户的原生 CFX 转账记录(ConfluxScan Open API) | 是(需登录) |
| 列出 Conflux Core 最新交易(ConfluxScan 浏览器 API) | 是(需登录) |
| 列出全网最新的 CFX 转账记录(ConfluxScan v1/transfer,默认测试网) | 是(需登录) |
| 列出全网 CFX 持仓地址排行榜(ConfluxScan stat/top-cfx-holder,默认测试网) | 是(需登录) |
/mcp默认要求登录(MCP_DEMO_REQUIRE_AUTH默认on):未携带有效令牌返回 401 +WWW-Authenticate,由客户端走标准 OAuth 流程。设成off后恢复匿名握手, 此时上表「匿名可用」的工具会照旧返回登录引导。
安全说明
所有页面输出经过 HTML 转义,用户名等用户输入不会造成 XSS。
Admin 鉴权两通道(
Cookiemcp_admin/X-Admin-Token请求头),令牌比较用timingSafeEqual; UI 任何链接都不把令牌拼进 URL(避免泄漏到地址栏 / 历史 / 截图 / Referer / 平台日志), 登录态靠mcp_adminCookie 保持。所有副作用操作均为 POST,并有登录失败限流与SameSite=Lax+ Origin 校验防 CSRF。公网环境下使用默认 admin 令牌会被直接禁用(503),强制要求注入真实
ADMIN_TOKEN。部署平台网关会改写
Authorization头,因此本服务永不信任传入的Authorization头传令牌。
已知限制
部署平台网关会剥离
Authorization头(实测:带不带 Cookie 都一样,只认X-Authorization/?token=/?access_token=,且值需带mcp_demo_前缀), 而官方 SDK 只会用Authorization头带令牌。结论:Discovery 与登录流程在本平台公网能跑通, 最后一跳(带令牌调/mcp)会被网关吞掉;本地直连 / 自托管无此限制。动态注册的客户端无法单独吊销:
client_id是自包含票据、不落盘,换JWT_SECRET才能让全部已注册客户端失效。授权码重放只在单进程内被拦截:「已用授权码」表是进程内
Map;防线退化为 「60 秒有效期 + PKCE」——攻击者必须同时截获授权码与code_verifier。refresh_token 不轮换:同理,无状态方案检测不到旧令牌是否被重放,轮换不会更安全。
磁盘:平台磁盘可能不持久化,重启/重新部署后数据可能丢失(退出前会尽量 flush)。
?token=(user 端访问令牌)会进入浏览器历史与平台访问日志,已用Referrer-Policy: no-referrer缓解;登录后该令牌会被落到mcp_demo_userCookie,后续免挂参数。admin 端已改用mcp_adminCookie,URL 中不再出现令牌。
This server cannot be deployed
Maintenance
Related MCP Connectors
Etherscan MCP — multichain block-explorer API (Etherscan V2)
Free read-only crypto whale-tracking & market-data MCP tools across 14 chains. No auth.
Solscan MCP — Solana block-explorer API (Pro v2)
Blockchair MCP — multi-chain block explorer (free tier, keyless).
Related MCP Servers
- AlicenseAqualityFmaintenanceProvides MCP tools to query Etherscan for Ethereum blockchain data—ETH balances, ERC-20 tokens, transactions, contract ABIs, and gas prices—without requiring an API key.813 npmMIT
- FlicenseNot gradedqualityDmaintenanceIntegrates multiple XRPL data sources including LOS, Validator History Service, XRPL JSON-RPC, and XRPLMeta to provide comprehensive querying of XRPL network data, accounts, transactions, tokens, validators, and more via MCP tools.-
- AlicenseAqualityCmaintenanceA read-only MCP server that queries multichain EVM on-chain data through Blockscout REST API, providing tools for address info, transactions, logs, and more.1611 npmMIT
- AlicenseNot gradedqualityBmaintenanceProvides multichain block-explorer data from Etherscan via MCP tools for token balances, contract ABI, and source code.294 npmMIT