thumbgate 1.23.2 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +15 -4
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/package.json +16 -4
- package/public/ai-malpractice-prevention.html +62 -5
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/gates-engine.js +98 -13
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/security-scanner.js +80 -10
- package/scripts/tool-registry.js +35 -1
- package/scripts/vector-store.js +1 -0
- package/src/api/server.js +161 -2
package/src/api/server.js
CHANGED
|
@@ -199,6 +199,10 @@ const {
|
|
|
199
199
|
} = require('../../scripts/rate-limiter');
|
|
200
200
|
const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
|
|
201
201
|
const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
|
|
202
|
+
const mcpOauth = require('../../scripts/mcp-oauth');
|
|
203
|
+
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
204
|
+
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
205
|
+
const oauthStore = mcpOauth.createStore();
|
|
202
206
|
const resendMailer = require('../../scripts/mailer/resend-mailer');
|
|
203
207
|
const {
|
|
204
208
|
buildContextFootprintReport,
|
|
@@ -748,16 +752,23 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
|
|
|
748
752
|
function getPublicMcpTools() {
|
|
749
753
|
return MCP_TOOLS.map((tool) => ({
|
|
750
754
|
name: tool.name,
|
|
755
|
+
...(tool.title ? { title: tool.title } : {}),
|
|
751
756
|
description: tool.description,
|
|
752
757
|
inputSchema: tool.inputSchema,
|
|
758
|
+
// Serve the tool-registry annotations (readOnlyHint/destructiveHint). Required
|
|
759
|
+
// by the Claude Connectors Directory (missing annotations = the #1 rejection
|
|
760
|
+
// cause) and used by MCP clients for permission prompts. Was being dropped here.
|
|
761
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
753
762
|
}));
|
|
754
763
|
}
|
|
755
764
|
|
|
756
765
|
function getServerCardTools() {
|
|
757
766
|
return MCP_TOOLS.map((tool) => ({
|
|
758
767
|
name: tool.name,
|
|
768
|
+
...(tool.title ? { title: tool.title } : {}),
|
|
759
769
|
description: tool.description,
|
|
760
770
|
inputSchema: tool.inputSchema,
|
|
771
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
761
772
|
}));
|
|
762
773
|
}
|
|
763
774
|
|
|
@@ -3860,12 +3871,56 @@ function createApiServer() {
|
|
|
3860
3871
|
tools: getPublicMcpTools(),
|
|
3861
3872
|
},
|
|
3862
3873
|
});
|
|
3874
|
+
} else if (msg.method === 'tools/call') {
|
|
3875
|
+
// Authenticated tool execution. Accept either an OAuth 2.1 access
|
|
3876
|
+
// token (audience-bound to this MCP server, RFC 8707) or a raw
|
|
3877
|
+
// ThumbGate API key, both via the Bearer header.
|
|
3878
|
+
const bearer = extractBearerToken(req);
|
|
3879
|
+
const resourceUrl = buildPublicUrl(hostedConfig, '/mcp');
|
|
3880
|
+
const oauthSession = mcpOauth.resolveAccessToken(oauthStore, bearer);
|
|
3881
|
+
// OAuth path: token must resolve AND be audience-bound to this server
|
|
3882
|
+
// (RFC 8707). Raw-key path: only an exact match to a configured
|
|
3883
|
+
// operator/admin key — never "any non-empty bearer".
|
|
3884
|
+
const adminKey = String(process.env.THUMBGATE_API_KEY || '').trim();
|
|
3885
|
+
const operatorKey = String(process.env.THUMBGATE_OPERATOR_KEY || '').trim();
|
|
3886
|
+
const rawKeyValid = Boolean(bearer) && ((adminKey && bearer === adminKey) || (operatorKey && bearer === operatorKey));
|
|
3887
|
+
const authed = oauthSession
|
|
3888
|
+
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
3889
|
+
: rawKeyValid;
|
|
3890
|
+
if (!authed) {
|
|
3891
|
+
res.writeHead(401, {
|
|
3892
|
+
'Content-Type': 'application/json',
|
|
3893
|
+
// RFC 9728: point unauthenticated clients at the resource metadata.
|
|
3894
|
+
'WWW-Authenticate': `Bearer resource_metadata="${buildPublicUrl(hostedConfig, '/.well-known/oauth-protected-resource')}"`,
|
|
3895
|
+
});
|
|
3896
|
+
res.end(JSON.stringify({
|
|
3897
|
+
jsonrpc: '2.0', id: msg.id,
|
|
3898
|
+
error: { code: -32001, message: 'Authentication required. Use OAuth 2.1 (see /.well-known/oauth-protected-resource) or a ThumbGate API key.' },
|
|
3899
|
+
}));
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
(async () => {
|
|
3903
|
+
try {
|
|
3904
|
+
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
3905
|
+
const name = msg.params && msg.params.name;
|
|
3906
|
+
const args = (msg.params && msg.params.arguments) || {};
|
|
3907
|
+
const result = await callTool(name, args);
|
|
3908
|
+
sendJson(res, 200, {
|
|
3909
|
+
jsonrpc: '2.0', id: msg.id,
|
|
3910
|
+
result: { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) }] },
|
|
3911
|
+
});
|
|
3912
|
+
} catch (err) {
|
|
3913
|
+
sendJson(res, 200, {
|
|
3914
|
+
jsonrpc: '2.0', id: msg.id,
|
|
3915
|
+
result: { isError: true, content: [{ type: 'text', text: String(err && err.message || err) }] },
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
})();
|
|
3863
3919
|
} else {
|
|
3864
|
-
// All other tool calls require auth — return method not found for unauthenticated
|
|
3865
3920
|
sendJson(res, 200, {
|
|
3866
3921
|
jsonrpc: '2.0',
|
|
3867
3922
|
id: msg.id,
|
|
3868
|
-
error: { code: -32601, message:
|
|
3923
|
+
error: { code: -32601, message: `Method not found: ${msg.method}` },
|
|
3869
3924
|
});
|
|
3870
3925
|
}
|
|
3871
3926
|
} catch (_e) {
|
|
@@ -5166,6 +5221,108 @@ async function addContext(){
|
|
|
5166
5221
|
return;
|
|
5167
5222
|
}
|
|
5168
5223
|
|
|
5224
|
+
// OAuth 2.1 discovery metadata for the remote MCP connector (RFC 9728 / RFC 8414).
|
|
5225
|
+
// Lets Claude discover how to authenticate before calling authenticated tools.
|
|
5226
|
+
if (isGetLikeRequest && pathname === '/.well-known/oauth-protected-resource') {
|
|
5227
|
+
sendJson(res, 200, mcpOauth.buildProtectedResourceMetadata(buildPublicUrl(hostedConfig, '')), {}, {
|
|
5228
|
+
headOnly: isHeadRequest,
|
|
5229
|
+
});
|
|
5230
|
+
return;
|
|
5231
|
+
}
|
|
5232
|
+
if (isGetLikeRequest && (pathname === '/.well-known/oauth-authorization-server' || pathname === '/.well-known/openid-configuration')) {
|
|
5233
|
+
sendJson(res, 200, mcpOauth.buildAuthServerMetadata(buildPublicUrl(hostedConfig, '')), {}, {
|
|
5234
|
+
headOnly: isHeadRequest,
|
|
5235
|
+
});
|
|
5236
|
+
return;
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
// --- OAuth 2.1 (PKCE) endpoints for the remote MCP connector ---
|
|
5240
|
+
// RFC 7591 Dynamic Client Registration.
|
|
5241
|
+
if (req.method === 'POST' && pathname === '/oauth/register') {
|
|
5242
|
+
let body = '';
|
|
5243
|
+
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
5244
|
+
req.on('end', () => {
|
|
5245
|
+
let parsed = {};
|
|
5246
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch { /* ignore */ }
|
|
5247
|
+
const reg = mcpOauth.registerClient(oauthStore, parsed);
|
|
5248
|
+
if (reg.error) { sendJson(res, 400, reg); return; }
|
|
5249
|
+
sendJson(res, 201, reg);
|
|
5250
|
+
});
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
// Authorization endpoint: GET renders consent, POST issues the code.
|
|
5254
|
+
if (pathname === '/oauth/authorize') {
|
|
5255
|
+
if (isGetLikeRequest) {
|
|
5256
|
+
const q = parsed.searchParams;
|
|
5257
|
+
const fields = ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'scope', 'state', 'resource'];
|
|
5258
|
+
const hidden = fields.map((f) => `<input type="hidden" name="${f}" value="${escapeHtmlAttribute(q.get(f) || '')}">`).join('\n');
|
|
5259
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Authorize ThumbGate</title>
|
|
5260
|
+
<style>body{font:15px system-ui;margin:0;background:#0b0b0c;color:#eee;display:flex;min-height:100vh;align-items:center;justify-content:center}
|
|
5261
|
+
.card{background:#161618;border:1px solid #2a2a2e;border-radius:12px;padding:28px;max-width:420px}
|
|
5262
|
+
input[type=password]{width:100%;padding:10px;margin:8px 0 16px;border-radius:8px;border:1px solid #2a2a2e;background:#0b0b0c;color:#eee}
|
|
5263
|
+
button{width:100%;padding:11px;border-radius:8px;border:0;background:#10b981;color:#04120c;font-weight:600;cursor:pointer}
|
|
5264
|
+
a{color:#8b9}</style></head><body><form class="card" method="post" action="/oauth/authorize">
|
|
5265
|
+
<h2>Authorize Claude → ThumbGate</h2>
|
|
5266
|
+
<p>Paste your ThumbGate API key to let this connector act as you. Get one with <code>npx thumbgate init</code> or from your <a href="/dashboard">dashboard</a>.</p>
|
|
5267
|
+
${hidden}
|
|
5268
|
+
<input type="password" name="api_key" placeholder="ThumbGate API key" autocomplete="off" required>
|
|
5269
|
+
<button type="submit" name="approve" value="yes">Approve</button>
|
|
5270
|
+
</form></body></html>`;
|
|
5271
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
if (req.method === 'POST') {
|
|
5275
|
+
let body = '';
|
|
5276
|
+
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
5277
|
+
req.on('end', () => {
|
|
5278
|
+
const form = new URLSearchParams(body);
|
|
5279
|
+
const redirectUri = form.get('redirect_uri') || '';
|
|
5280
|
+
const state = form.get('state') || '';
|
|
5281
|
+
const issued = mcpOauth.createAuthorizationCode(oauthStore, {
|
|
5282
|
+
clientId: form.get('client_id') || '',
|
|
5283
|
+
redirectUri,
|
|
5284
|
+
codeChallenge: form.get('code_challenge') || '',
|
|
5285
|
+
codeChallengeMethod: form.get('code_challenge_method') || '',
|
|
5286
|
+
scope: form.get('scope') || undefined,
|
|
5287
|
+
resource: form.get('resource') || buildPublicUrl(hostedConfig, '/mcp'),
|
|
5288
|
+
boundKey: form.get('api_key') || '',
|
|
5289
|
+
state,
|
|
5290
|
+
});
|
|
5291
|
+
if (issued.error) {
|
|
5292
|
+
sendJson(res, 400, { error: issued.error, error_description: issued.error_description });
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
const sep = redirectUri.includes('?') ? '&' : '?';
|
|
5296
|
+
const loc = `${redirectUri}${sep}code=${encodeURIComponent(issued.code)}${state ? `&state=${encodeURIComponent(state)}` : ''}`;
|
|
5297
|
+
res.writeHead(302, { Location: loc });
|
|
5298
|
+
res.end();
|
|
5299
|
+
});
|
|
5300
|
+
return;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
// Token endpoint (authorization_code + PKCE).
|
|
5304
|
+
if (req.method === 'POST' && pathname === '/oauth/token') {
|
|
5305
|
+
let body = '';
|
|
5306
|
+
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy(); });
|
|
5307
|
+
req.on('end', () => {
|
|
5308
|
+
const form = new URLSearchParams(body);
|
|
5309
|
+
if (form.get('grant_type') !== 'authorization_code') {
|
|
5310
|
+
sendJson(res, 400, { error: 'unsupported_grant_type' });
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5313
|
+
const tok = mcpOauth.exchangeCode(oauthStore, {
|
|
5314
|
+
code: form.get('code') || '',
|
|
5315
|
+
codeVerifier: form.get('code_verifier') || '',
|
|
5316
|
+
clientId: form.get('client_id') || '',
|
|
5317
|
+
redirectUri: form.get('redirect_uri') || '',
|
|
5318
|
+
resource: form.get('resource') || undefined,
|
|
5319
|
+
});
|
|
5320
|
+
if (tok.error) { sendJson(res, 400, tok); return; }
|
|
5321
|
+
sendJson(res, 200, tok, { 'Cache-Control': 'no-store' });
|
|
5322
|
+
});
|
|
5323
|
+
return;
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5169
5326
|
if (isGetLikeRequest && pathname === '/.well-known/mcp/tools.json') {
|
|
5170
5327
|
sendJson(res, 200, {
|
|
5171
5328
|
name: 'thumbgate',
|
|
@@ -8072,6 +8229,8 @@ module.exports = {
|
|
|
8072
8229
|
renderPackagedLessonsHtml,
|
|
8073
8230
|
readOptionalPublicTemplate,
|
|
8074
8231
|
resolveLocalPageBootstrap,
|
|
8232
|
+
getPublicMcpTools,
|
|
8233
|
+
getServerCardTools,
|
|
8075
8234
|
},
|
|
8076
8235
|
};
|
|
8077
8236
|
|