trapic-mcp 0.1.0 → 0.1.1
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/bin/trapic-mcp.mjs +92 -0
- package/dist/worker.js +49 -4
- package/package.json +1 -1
package/bin/trapic-mcp.mjs
CHANGED
|
@@ -379,9 +379,16 @@ async function cmdServeDev() {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
async function cmdLogin() {
|
|
382
|
+
const useBrowser = process.argv.includes("--browser") || process.argv.includes("-b");
|
|
383
|
+
|
|
384
|
+
if (useBrowser) {
|
|
385
|
+
return cmdLoginBrowser();
|
|
386
|
+
}
|
|
387
|
+
|
|
382
388
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
383
389
|
|
|
384
390
|
try {
|
|
391
|
+
console.log("Login with email/password. For GitHub/Google, use: trapic login --browser\n");
|
|
385
392
|
const serverUrl =
|
|
386
393
|
(await rl.question(`Server URL [${DEFAULT_URL}]: `)).trim() || DEFAULT_URL;
|
|
387
394
|
const email = (await rl.question("Email: ")).trim();
|
|
@@ -483,6 +490,90 @@ async function cmdLogin() {
|
|
|
483
490
|
}
|
|
484
491
|
}
|
|
485
492
|
|
|
493
|
+
async function cmdLoginBrowser() {
|
|
494
|
+
const { exec } = await import("child_process");
|
|
495
|
+
|
|
496
|
+
console.log("Opening browser to create an API token...\n");
|
|
497
|
+
|
|
498
|
+
const tokenUrl = "https://trapic.ai/tokens";
|
|
499
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
500
|
+
exec(`${openCmd} ${tokenUrl}`);
|
|
501
|
+
|
|
502
|
+
console.log("1. Sign in with GitHub, Google, or email in the browser");
|
|
503
|
+
console.log("2. Create a new API token");
|
|
504
|
+
console.log("3. Copy the token and paste it below\n");
|
|
505
|
+
|
|
506
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
507
|
+
const token = (await rl.question("Paste your API token (tr_...): ")).trim();
|
|
508
|
+
rl.close();
|
|
509
|
+
|
|
510
|
+
if (!token || !token.startsWith("tr_")) {
|
|
511
|
+
console.error("Error: Invalid token. Must start with tr_");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Verify token
|
|
516
|
+
const res = await fetch("https://mcp.trapic.ai/auth/whoami", {
|
|
517
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (!res.ok) {
|
|
521
|
+
console.error("Error: Token verification failed. Check your token and try again.");
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const data = await res.json();
|
|
526
|
+
const mcpUrl = "https://mcp.trapic.ai/mcp";
|
|
527
|
+
await saveCredentials({ url: mcpUrl, token, email: data.user?.email || "" });
|
|
528
|
+
console.log(`\nLogged in as ${data.user?.email || "unknown"}`);
|
|
529
|
+
console.log(`Credentials saved to ${CRED_FILE}`);
|
|
530
|
+
|
|
531
|
+
// Configure Claude Code
|
|
532
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
533
|
+
try {
|
|
534
|
+
const setupClaude = (
|
|
535
|
+
await rl2.question("\nConfigure for Claude Code? [Y/n]: ")
|
|
536
|
+
).trim().toLowerCase();
|
|
537
|
+
|
|
538
|
+
if (setupClaude !== "n" && setupClaude !== "no") {
|
|
539
|
+
const scope = (
|
|
540
|
+
await rl2.question("Scope — global or project? [global]: ")
|
|
541
|
+
).trim().toLowerCase();
|
|
542
|
+
|
|
543
|
+
const mcpEntry = {
|
|
544
|
+
command: "npx",
|
|
545
|
+
args: ["-y", "trapic-mcp"],
|
|
546
|
+
env: { TRAPIC_TOKEN: token },
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (scope === "project" || scope === "p") {
|
|
550
|
+
const projectDir = process.cwd();
|
|
551
|
+
const claudeDir = join(projectDir, ".claude");
|
|
552
|
+
const claudeJson = join(claudeDir, "mcp.json");
|
|
553
|
+
await mkdir(claudeDir, { recursive: true });
|
|
554
|
+
let existing = {};
|
|
555
|
+
try { existing = JSON.parse(await readFile(claudeJson, "utf-8")); } catch {}
|
|
556
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
557
|
+
existing.mcpServers.trapic = mcpEntry;
|
|
558
|
+
await writeFile(claudeJson, JSON.stringify(existing, null, 2) + "\n");
|
|
559
|
+
console.log(`Saved to ${claudeJson}`);
|
|
560
|
+
} else {
|
|
561
|
+
const globalDir = join(homedir(), ".claude");
|
|
562
|
+
const globalJson = join(globalDir, "mcp.json");
|
|
563
|
+
await mkdir(globalDir, { recursive: true });
|
|
564
|
+
let existing = {};
|
|
565
|
+
try { existing = JSON.parse(await readFile(globalJson, "utf-8")); } catch {}
|
|
566
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
567
|
+
existing.mcpServers.trapic = mcpEntry;
|
|
568
|
+
await writeFile(globalJson, JSON.stringify(existing, null, 2) + "\n");
|
|
569
|
+
console.log(`Saved to ${globalJson}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} finally {
|
|
573
|
+
rl2.close();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
486
577
|
async function cmdLogout() {
|
|
487
578
|
const config = await loadConfig();
|
|
488
579
|
if (!config) {
|
|
@@ -879,6 +970,7 @@ switch (command) {
|
|
|
879
970
|
Usage:
|
|
880
971
|
trapic-mcp Start stdio proxy (for Claude Code)
|
|
881
972
|
trapic-mcp login Login with email/password
|
|
973
|
+
trapic-mcp login --browser Login with GitHub/Google (opens browser)
|
|
882
974
|
trapic-mcp logout Revoke token and clear credentials
|
|
883
975
|
trapic-mcp whoami Show current user and token info
|
|
884
976
|
trapic-mcp init Manual setup with token (--global | --project)
|
package/dist/worker.js
CHANGED
|
@@ -110,7 +110,7 @@ function getCorsOrigin(request) {
|
|
|
110
110
|
function isValidRedirectUri(uri) {
|
|
111
111
|
try {
|
|
112
112
|
const parsed = new URL(uri);
|
|
113
|
-
const allowed = ["localhost", "127.0.0.1", "trapic.ai"];
|
|
113
|
+
const allowed = ["localhost", "127.0.0.1", "trapic.ai", "claude.ai"];
|
|
114
114
|
return allowed.some(h => parsed.hostname === h || parsed.hostname.endsWith("." + h));
|
|
115
115
|
}
|
|
116
116
|
catch {
|
|
@@ -249,8 +249,12 @@ export default {
|
|
|
249
249
|
label { display: block; font-size: 0.8rem; color: #a3a3a3; margin-bottom: 0.25rem; }
|
|
250
250
|
input[type="email"], input[type="password"] { width: 100%; padding: 0.6rem 0.75rem; background: #0a0a0a; border: 1px solid #333; border-radius: 8px; color: #e5e5e5; font-size: 0.9rem; margin-bottom: 1rem; outline: none; }
|
|
251
251
|
input:focus { border-color: #525252; }
|
|
252
|
-
button { width: 100%; padding: 0.6rem; background: #fff; color: #000; border: none; border-radius: 9999px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
|
|
253
|
-
button:hover { opacity: 0.9; }
|
|
252
|
+
button, .oauth-btn { width: 100%; padding: 0.6rem; background: #fff; color: #000; border: none; border-radius: 9999px; font-size: 0.9rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; }
|
|
253
|
+
button:hover, .oauth-btn:hover { opacity: 0.9; }
|
|
254
|
+
.oauth-btn { background: #262626; color: #e5e5e5; border: 1px solid #333; margin-bottom: 0.5rem; }
|
|
255
|
+
.divider { display: flex; align-items: center; gap: 0.75rem; margin: 1.25rem 0; }
|
|
256
|
+
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #333; }
|
|
257
|
+
.divider span { font-size: 0.75rem; color: #525252; }
|
|
254
258
|
.error { color: #ef4444; font-size: 0.8rem; margin-bottom: 1rem; }
|
|
255
259
|
</style>
|
|
256
260
|
</head>
|
|
@@ -259,6 +263,15 @@ export default {
|
|
|
259
263
|
<div class="logo">Trapic</div>
|
|
260
264
|
<div class="desc">Sign in to authorize access to your knowledge base</div>
|
|
261
265
|
${errorMsg ? `<div class="error">${escapeHtml(errorMsg)}</div>` : ""}
|
|
266
|
+
<a class="oauth-btn" href="${env.SUPABASE_URL}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(origin + '/oauth/callback-social?' + new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, state, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }).toString())}">
|
|
267
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
268
|
+
Continue with GitHub
|
|
269
|
+
</a>
|
|
270
|
+
<a class="oauth-btn" href="${env.SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(origin + '/oauth/callback-social?' + new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, state, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }).toString())}">
|
|
271
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
|
|
272
|
+
Continue with Google
|
|
273
|
+
</a>
|
|
274
|
+
<div class="divider"><span>or</span></div>
|
|
262
275
|
<form method="POST" action="/oauth/authorize">
|
|
263
276
|
<input type="hidden" name="client_id" value="${escapeHtml(clientId)}" />
|
|
264
277
|
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}" />
|
|
@@ -266,7 +279,7 @@ export default {
|
|
|
266
279
|
<input type="hidden" name="code_challenge" value="${escapeHtml(codeChallenge)}" />
|
|
267
280
|
<input type="hidden" name="code_challenge_method" value="${escapeHtml(codeChallengeMethod)}" />
|
|
268
281
|
<label>Email</label>
|
|
269
|
-
<input type="email" name="email" required autocomplete="email"
|
|
282
|
+
<input type="email" name="email" required autocomplete="email" />
|
|
270
283
|
<label>Password</label>
|
|
271
284
|
<input type="password" name="password" required autocomplete="current-password" />
|
|
272
285
|
<button type="submit">Sign In</button>
|
|
@@ -283,6 +296,38 @@ export default {
|
|
|
283
296
|
},
|
|
284
297
|
});
|
|
285
298
|
}
|
|
299
|
+
// OAuth social callback — Supabase redirects here after GitHub/Google login
|
|
300
|
+
if (url.pathname === "/oauth/callback-social") {
|
|
301
|
+
const accessToken = url.hash ? new URLSearchParams(url.hash.slice(1)).get("access_token") : url.searchParams.get("access_token");
|
|
302
|
+
const clientId = url.searchParams.get("client_id") || "";
|
|
303
|
+
const redirectUri = url.searchParams.get("redirect_uri") || "";
|
|
304
|
+
const state = url.searchParams.get("state") || "";
|
|
305
|
+
const codeChallenge = url.searchParams.get("code_challenge") || "";
|
|
306
|
+
if (!redirectUri || !codeChallenge) {
|
|
307
|
+
return new Response("Missing OAuth parameters", { status: 400 });
|
|
308
|
+
}
|
|
309
|
+
// Get user from Supabase access token
|
|
310
|
+
const supabase = getSupabase();
|
|
311
|
+
const { data: { user: authUser }, error: authErr } = await supabase.auth.getUser(accessToken || "");
|
|
312
|
+
if (authErr || !authUser) {
|
|
313
|
+
// Redirect back to authorize with error
|
|
314
|
+
const backUrl = new URL(`${origin}/oauth/authorize`);
|
|
315
|
+
backUrl.searchParams.set("client_id", clientId);
|
|
316
|
+
backUrl.searchParams.set("redirect_uri", redirectUri);
|
|
317
|
+
backUrl.searchParams.set("state", state);
|
|
318
|
+
backUrl.searchParams.set("code_challenge", codeChallenge);
|
|
319
|
+
backUrl.searchParams.set("code_challenge_method", "S256");
|
|
320
|
+
backUrl.searchParams.set("error", "Social login failed. Please try again.");
|
|
321
|
+
return new Response(null, { status: 302, headers: { "Location": backUrl.toString() } });
|
|
322
|
+
}
|
|
323
|
+
// Generate signed auth code
|
|
324
|
+
const code = await createAuthCode(env, authUser.id, codeChallenge, redirectUri);
|
|
325
|
+
const redirectUrl = new URL(redirectUri);
|
|
326
|
+
redirectUrl.searchParams.set("code", code);
|
|
327
|
+
if (state)
|
|
328
|
+
redirectUrl.searchParams.set("state", state);
|
|
329
|
+
return new Response(null, { status: 302, headers: { "Location": redirectUrl.toString() } });
|
|
330
|
+
}
|
|
286
331
|
// OAuth Authorize POST — validates credentials, issues auth code via 302 redirect
|
|
287
332
|
if (url.pathname === "/oauth/authorize" && request.method === "POST") {
|
|
288
333
|
// Parse form body (native form POST sends application/x-www-form-urlencoded)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trapic-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "AI knowledge management MCP server — automatically capture, search, and connect decisions, facts, and conventions across AI coding sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|