@atoms-tech/atoms-mcp 0.3.0 → 0.3.2
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/README.md +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.js +1 -90
- package/package.json +12 -3
- package/dist/apps/register.d.ts +0 -36
- package/dist/apps/register.js +0 -65
- package/dist/auth/login.d.ts +0 -24
- package/dist/auth/login.js +0 -413
- package/dist/auth/refresh.d.ts +0 -16
- package/dist/auth/refresh.js +0 -81
- package/dist/auth/token-store.d.ts +0 -33
- package/dist/auth/token-store.js +0 -60
- package/dist/config.d.ts +0 -18
- package/dist/config.js +0 -18
- package/dist/db/client.d.ts +0 -29
- package/dist/db/client.js +0 -109
- package/dist/db/graph.d.ts +0 -7
- package/dist/db/graph.js +0 -7
- package/dist/db/queries.d.ts +0 -76
- package/dist/db/queries.js +0 -209
- package/dist/index.d.ts +0 -11
- package/dist/middleware/audit.d.ts +0 -25
- package/dist/middleware/audit.js +0 -43
- package/dist/middleware/rate-limiter.d.ts +0 -20
- package/dist/middleware/rate-limiter.js +0 -42
- package/dist/middleware/validator.d.ts +0 -21
- package/dist/middleware/validator.js +0 -90
- package/dist/server.d.ts +0 -14
- package/dist/server.js +0 -679
- package/dist/tools/_base.d.ts +0 -57
- package/dist/tools/_base.js +0 -108
- package/dist/tools/bulk-import.d.ts +0 -69
- package/dist/tools/bulk-import.js +0 -187
- package/dist/tools/create-item.d.ts +0 -42
- package/dist/tools/create-item.js +0 -117
- package/dist/tools/delete-item.d.ts +0 -37
- package/dist/tools/delete-item.js +0 -68
- package/dist/tools/export-mermaid.d.ts +0 -35
- package/dist/tools/export-mermaid.js +0 -124
- package/dist/tools/get-coverage.d.ts +0 -33
- package/dist/tools/get-coverage.js +0 -35
- package/dist/tools/get-history.d.ts +0 -33
- package/dist/tools/get-history.js +0 -52
- package/dist/tools/get-item.d.ts +0 -61
- package/dist/tools/get-item.js +0 -92
- package/dist/tools/link-items.d.ts +0 -40
- package/dist/tools/link-items.js +0 -149
- package/dist/tools/list-items.d.ts +0 -36
- package/dist/tools/list-items.js +0 -35
- package/dist/tools/list-projects.d.ts +0 -37
- package/dist/tools/list-projects.js +0 -27
- package/dist/tools/project-summary.d.ts +0 -63
- package/dist/tools/project-summary.js +0 -169
- package/dist/tools/record-test-result.d.ts +0 -40
- package/dist/tools/record-test-result.js +0 -79
- package/dist/tools/search.d.ts +0 -33
- package/dist/tools/search.js +0 -27
- package/dist/tools/trace.d.ts +0 -52
- package/dist/tools/trace.js +0 -165
- package/dist/tools/update-item.d.ts +0 -42
- package/dist/tools/update-item.js +0 -97
- package/dist/types/responses.d.ts +0 -57
- package/dist/types/responses.js +0 -5
- package/dist/types/work-item.d.ts +0 -68
- package/dist/types/work-item.js +0 -7
package/dist/auth/login.js
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OAuth 2.1 PKCE login flow for the MCP CLI.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. Generate PKCE code_verifier + code_challenge
|
|
6
|
-
* 2. Start HTTP server on localhost:19275
|
|
7
|
-
* 3. Open browser to x.atoms.tech/auth/mcp-login (or Supabase auth directly)
|
|
8
|
-
* 4. User authenticates → redirect to localhost with tokens
|
|
9
|
-
* 5. Store tokens in ~/.atoms/credentials.json
|
|
10
|
-
*/
|
|
11
|
-
import { createServer } from "node:http";
|
|
12
|
-
import { randomBytes, createHash } from "node:crypto";
|
|
13
|
-
import { URL } from "node:url";
|
|
14
|
-
import { writeCredentials } from "./token-store.js";
|
|
15
|
-
import { ATOMS_SUPABASE_URL, ATOMS_SUPABASE_ANON_KEY, ATOMS_APP_URL } from "../config.js";
|
|
16
|
-
const CALLBACK_PORT = 19275;
|
|
17
|
-
const CALLBACK_PATH = "/callback";
|
|
18
|
-
/**
|
|
19
|
-
* Run the interactive login flow.
|
|
20
|
-
* Opens browser, waits for callback, stores tokens.
|
|
21
|
-
*
|
|
22
|
-
* Uses OAuth 2.1 PKCE flow with hosted consent page:
|
|
23
|
-
* 1. Open browser to ATOMS consent page with code_challenge
|
|
24
|
-
* 2. User reviews permissions and clicks "Authorize"
|
|
25
|
-
* 3. Consent page redirects to Supabase OAuth with PKCE params
|
|
26
|
-
* 4. Supabase returns authorization code to localhost callback
|
|
27
|
-
* 5. Exchange code + code_verifier for session tokens
|
|
28
|
-
*/
|
|
29
|
-
export async function login() {
|
|
30
|
-
// Generate PKCE challenge
|
|
31
|
-
const codeVerifier = randomBytes(32).toString("base64url");
|
|
32
|
-
const codeChallenge = createHash("sha256")
|
|
33
|
-
.update(codeVerifier)
|
|
34
|
-
.digest("base64url");
|
|
35
|
-
const redirectTo = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
36
|
-
// Build consent page URL — hosted on ATOMS web app
|
|
37
|
-
const authUrl = new URL(`${ATOMS_APP_URL}/auth/mcp-consent`);
|
|
38
|
-
authUrl.searchParams.set("redirect_uri", redirectTo);
|
|
39
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
40
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
const server = createServer(async (req, res) => {
|
|
43
|
-
const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
|
|
44
|
-
if (url.pathname !== CALLBACK_PATH) {
|
|
45
|
-
res.writeHead(404);
|
|
46
|
-
res.end("Not found");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
// Check for user cancellation from consent page
|
|
51
|
-
const error = url.searchParams.get("error");
|
|
52
|
-
if (error === "access_denied") {
|
|
53
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
54
|
-
res.end(failurePage("Authorization was cancelled."));
|
|
55
|
-
server.close();
|
|
56
|
-
reject(new Error("Authorization cancelled by user."));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
// PKCE flow: Supabase redirects with ?code=xxx in query params
|
|
60
|
-
const code = url.searchParams.get("code");
|
|
61
|
-
if (code) {
|
|
62
|
-
// Exchange authorization code + code_verifier for tokens via REST API
|
|
63
|
-
const tokenRes = await fetch(`${ATOMS_SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: {
|
|
66
|
-
"Content-Type": "application/json",
|
|
67
|
-
"apikey": ATOMS_SUPABASE_ANON_KEY,
|
|
68
|
-
},
|
|
69
|
-
body: JSON.stringify({
|
|
70
|
-
auth_code: code,
|
|
71
|
-
code_verifier: codeVerifier,
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
const tokenData = await tokenRes.json();
|
|
75
|
-
if (!tokenRes.ok || !tokenData.access_token) {
|
|
76
|
-
const errMsg = tokenData.error_description ?? tokenData.msg ?? "Unknown error";
|
|
77
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
78
|
-
res.end(failurePage(`Code exchange failed: ${errMsg}`));
|
|
79
|
-
server.close();
|
|
80
|
-
reject(new Error(`Code exchange failed: ${errMsg}`));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
// Decode JWT to get email
|
|
84
|
-
let email = "authenticated";
|
|
85
|
-
try {
|
|
86
|
-
const payload = JSON.parse(Buffer.from(tokenData.access_token.split(".")[1], "base64").toString());
|
|
87
|
-
email = payload.email ?? email;
|
|
88
|
-
}
|
|
89
|
-
catch { /* ignore decode errors */ }
|
|
90
|
-
// Verify user has an ATOMS account (belongs to at least one org)
|
|
91
|
-
const { createClient } = await import("@supabase/supabase-js");
|
|
92
|
-
const verifyClient = createClient(ATOMS_SUPABASE_URL, ATOMS_SUPABASE_ANON_KEY, {
|
|
93
|
-
global: {
|
|
94
|
-
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
const { data: memberships, error: memErr } = await verifyClient
|
|
98
|
-
.from("org_members")
|
|
99
|
-
.select("org_id")
|
|
100
|
-
.limit(1);
|
|
101
|
-
if (memErr || !memberships || memberships.length === 0) {
|
|
102
|
-
// User authenticated with Google but has no ATOMS account
|
|
103
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
104
|
-
res.end(failurePage("No ATOMS account found for this Google account.<br><br>" +
|
|
105
|
-
"Please sign up at <a href=\"https://x.atoms.tech\">x.atoms.tech</a> first, " +
|
|
106
|
-
"then run this login command again."));
|
|
107
|
-
server.close();
|
|
108
|
-
reject(new Error("No ATOMS account found. Sign up at x.atoms.tech first."));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
await writeCredentials({
|
|
112
|
-
access_token: tokenData.access_token,
|
|
113
|
-
refresh_token: tokenData.refresh_token,
|
|
114
|
-
expires_at: Date.now() + (tokenData.expires_in ?? 3600) * 1000,
|
|
115
|
-
user_email: email,
|
|
116
|
-
});
|
|
117
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
118
|
-
res.end(successPage(email));
|
|
119
|
-
server.close();
|
|
120
|
-
resolve({ email });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
// Fallback: check for tokens in hash fragment via extractor page
|
|
124
|
-
const accessToken = url.searchParams.get("access_token");
|
|
125
|
-
const refreshToken = url.searchParams.get("refresh_token");
|
|
126
|
-
const expiresIn = parseInt(url.searchParams.get("expires_in") ?? "3600", 10);
|
|
127
|
-
if (accessToken && refreshToken) {
|
|
128
|
-
await writeCredentials({
|
|
129
|
-
access_token: accessToken,
|
|
130
|
-
refresh_token: refreshToken,
|
|
131
|
-
expires_at: Date.now() + expiresIn * 1000,
|
|
132
|
-
});
|
|
133
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
134
|
-
res.end(successPage("authenticated"));
|
|
135
|
-
server.close();
|
|
136
|
-
resolve({ email: "authenticated" });
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// Serve a page that extracts hash fragment and POSTs back
|
|
140
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
141
|
-
res.end(extractorPage());
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
res.writeHead(500);
|
|
146
|
-
res.end("Authentication failed");
|
|
147
|
-
server.close();
|
|
148
|
-
reject(err);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
// Handle POST from the extractor page (hash fragment fallback)
|
|
152
|
-
server.on("request", async (req, res) => {
|
|
153
|
-
if (req.method === "POST" && req.url === "/token") {
|
|
154
|
-
let body = "";
|
|
155
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
156
|
-
req.on("end", async () => {
|
|
157
|
-
try {
|
|
158
|
-
const data = JSON.parse(body);
|
|
159
|
-
await writeCredentials({
|
|
160
|
-
access_token: data.access_token,
|
|
161
|
-
refresh_token: data.refresh_token,
|
|
162
|
-
expires_at: Date.now() + (data.expires_in ?? 3600) * 1000,
|
|
163
|
-
user_email: data.user_email,
|
|
164
|
-
});
|
|
165
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
166
|
-
res.end(JSON.stringify({ ok: true }));
|
|
167
|
-
server.close();
|
|
168
|
-
resolve({ email: data.user_email ?? "authenticated" });
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
res.writeHead(500);
|
|
172
|
-
res.end("Failed to store credentials");
|
|
173
|
-
server.close();
|
|
174
|
-
reject(err);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
server.listen(CALLBACK_PORT, "127.0.0.1", async () => {
|
|
180
|
-
process.stderr.write(`\nOpening browser for ATOMS login...\n`);
|
|
181
|
-
process.stderr.write(`If the browser doesn't open, visit:\n${authUrl.toString()}\n\n`);
|
|
182
|
-
try {
|
|
183
|
-
const { default: openBrowser } = await import("open");
|
|
184
|
-
await openBrowser(authUrl.toString());
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
process.stderr.write("Could not open browser automatically. Please open the URL above.\n");
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
// Timeout after 5 minutes
|
|
191
|
-
setTimeout(() => {
|
|
192
|
-
server.close();
|
|
193
|
-
reject(new Error("Login timed out after 5 minutes"));
|
|
194
|
-
}, 5 * 60_000);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
function extractorPage() {
|
|
198
|
-
return `<!DOCTYPE html>
|
|
199
|
-
<html>
|
|
200
|
-
<head><title>ATOMS MCP Login</title></head>
|
|
201
|
-
<body>
|
|
202
|
-
<h1>Completing login...</h1>
|
|
203
|
-
<script>
|
|
204
|
-
const hash = window.location.hash.substring(1);
|
|
205
|
-
const params = new URLSearchParams(hash);
|
|
206
|
-
const data = {
|
|
207
|
-
access_token: params.get('access_token'),
|
|
208
|
-
refresh_token: params.get('refresh_token'),
|
|
209
|
-
expires_in: params.get('expires_in'),
|
|
210
|
-
user_email: ''
|
|
211
|
-
};
|
|
212
|
-
if (data.access_token) {
|
|
213
|
-
// Decode JWT to get email
|
|
214
|
-
try {
|
|
215
|
-
const payload = JSON.parse(atob(data.access_token.split('.')[1]));
|
|
216
|
-
data.user_email = payload.email || '';
|
|
217
|
-
} catch(e) {}
|
|
218
|
-
fetch('/token', {
|
|
219
|
-
method: 'POST',
|
|
220
|
-
headers: { 'Content-Type': 'application/json' },
|
|
221
|
-
body: JSON.stringify(data)
|
|
222
|
-
}).then(() => {
|
|
223
|
-
document.body.innerHTML = '<h1>Login successful! You can close this tab.</h1>';
|
|
224
|
-
}).catch(() => {
|
|
225
|
-
document.body.innerHTML = '<h1>Login failed. Please try again.</h1>';
|
|
226
|
-
});
|
|
227
|
-
} else {
|
|
228
|
-
document.body.innerHTML = '<h1>Login failed — no tokens received.</h1>';
|
|
229
|
-
}
|
|
230
|
-
</script>
|
|
231
|
-
</body>
|
|
232
|
-
</html>`;
|
|
233
|
-
}
|
|
234
|
-
function successPage(email) {
|
|
235
|
-
return `<!DOCTYPE html>
|
|
236
|
-
<html>
|
|
237
|
-
<head>
|
|
238
|
-
<title>ATOMS MCP — Authorized</title>
|
|
239
|
-
<style>
|
|
240
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
241
|
-
body {
|
|
242
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
243
|
-
background: #0a0a0a;
|
|
244
|
-
color: #fafafa;
|
|
245
|
-
display: flex;
|
|
246
|
-
align-items: center;
|
|
247
|
-
justify-content: center;
|
|
248
|
-
min-height: 100vh;
|
|
249
|
-
}
|
|
250
|
-
.card {
|
|
251
|
-
background: #171717;
|
|
252
|
-
border: 1px solid #262626;
|
|
253
|
-
border-radius: 16px;
|
|
254
|
-
padding: 48px;
|
|
255
|
-
max-width: 480px;
|
|
256
|
-
width: 100%;
|
|
257
|
-
text-align: center;
|
|
258
|
-
}
|
|
259
|
-
.icon {
|
|
260
|
-
width: 56px;
|
|
261
|
-
height: 56px;
|
|
262
|
-
background: #052e16;
|
|
263
|
-
border-radius: 50%;
|
|
264
|
-
display: flex;
|
|
265
|
-
align-items: center;
|
|
266
|
-
justify-content: center;
|
|
267
|
-
margin: 0 auto 24px;
|
|
268
|
-
}
|
|
269
|
-
.icon svg { width: 28px; height: 28px; }
|
|
270
|
-
h1 {
|
|
271
|
-
font-size: 22px;
|
|
272
|
-
font-weight: 600;
|
|
273
|
-
margin-bottom: 8px;
|
|
274
|
-
color: #22c55e;
|
|
275
|
-
}
|
|
276
|
-
.email {
|
|
277
|
-
font-size: 14px;
|
|
278
|
-
color: #a3a3a3;
|
|
279
|
-
margin-bottom: 32px;
|
|
280
|
-
}
|
|
281
|
-
.permissions {
|
|
282
|
-
text-align: left;
|
|
283
|
-
background: #0a0a0a;
|
|
284
|
-
border: 1px solid #262626;
|
|
285
|
-
border-radius: 12px;
|
|
286
|
-
padding: 20px;
|
|
287
|
-
margin-bottom: 24px;
|
|
288
|
-
}
|
|
289
|
-
.permissions h3 {
|
|
290
|
-
font-size: 12px;
|
|
291
|
-
font-weight: 600;
|
|
292
|
-
color: #737373;
|
|
293
|
-
text-transform: uppercase;
|
|
294
|
-
letter-spacing: 0.05em;
|
|
295
|
-
margin-bottom: 12px;
|
|
296
|
-
}
|
|
297
|
-
.perm-item {
|
|
298
|
-
display: flex;
|
|
299
|
-
align-items: center;
|
|
300
|
-
gap: 10px;
|
|
301
|
-
padding: 6px 0;
|
|
302
|
-
font-size: 14px;
|
|
303
|
-
color: #d4d4d4;
|
|
304
|
-
}
|
|
305
|
-
.perm-item .dot {
|
|
306
|
-
width: 6px;
|
|
307
|
-
height: 6px;
|
|
308
|
-
background: #22c55e;
|
|
309
|
-
border-radius: 50%;
|
|
310
|
-
flex-shrink: 0;
|
|
311
|
-
}
|
|
312
|
-
.closing {
|
|
313
|
-
font-size: 13px;
|
|
314
|
-
color: #525252;
|
|
315
|
-
}
|
|
316
|
-
.closing span { color: #a3a3a3; font-variant-numeric: tabular-nums; }
|
|
317
|
-
</style>
|
|
318
|
-
</head>
|
|
319
|
-
<body>
|
|
320
|
-
<div class="card">
|
|
321
|
-
<div class="icon">
|
|
322
|
-
<svg fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">
|
|
323
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
324
|
-
</svg>
|
|
325
|
-
</div>
|
|
326
|
-
<h1>Authorization Granted</h1>
|
|
327
|
-
<p class="email">${email}</p>
|
|
328
|
-
|
|
329
|
-
<div class="permissions">
|
|
330
|
-
<h3>ATOMS MCP can now</h3>
|
|
331
|
-
<div class="perm-item"><span class="dot"></span> Read your projects and requirements</div>
|
|
332
|
-
<div class="perm-item"><span class="dot"></span> Create and edit requirements and test cases</div>
|
|
333
|
-
<div class="perm-item"><span class="dot"></span> Record test results (pass/fail)</div>
|
|
334
|
-
<div class="perm-item"><span class="dot"></span> Manage traceability relationships</div>
|
|
335
|
-
<div class="perm-item"><span class="dot"></span> Export coverage reports and diagrams</div>
|
|
336
|
-
</div>
|
|
337
|
-
|
|
338
|
-
<p class="closing">This tab will close in <span id="countdown">5</span>s</p>
|
|
339
|
-
</div>
|
|
340
|
-
|
|
341
|
-
<script>
|
|
342
|
-
let seconds = 5;
|
|
343
|
-
const el = document.getElementById('countdown');
|
|
344
|
-
const timer = setInterval(() => {
|
|
345
|
-
seconds--;
|
|
346
|
-
el.textContent = seconds;
|
|
347
|
-
if (seconds <= 0) {
|
|
348
|
-
clearInterval(timer);
|
|
349
|
-
window.close();
|
|
350
|
-
// If window.close() is blocked (not opened by script), update message
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
document.querySelector('.closing').textContent = 'You can close this tab now.';
|
|
353
|
-
}, 500);
|
|
354
|
-
}
|
|
355
|
-
}, 1000);
|
|
356
|
-
</script>
|
|
357
|
-
</body>
|
|
358
|
-
</html>`;
|
|
359
|
-
}
|
|
360
|
-
function failurePage(reason) {
|
|
361
|
-
return `<!DOCTYPE html>
|
|
362
|
-
<html>
|
|
363
|
-
<head>
|
|
364
|
-
<title>ATOMS MCP — Failed</title>
|
|
365
|
-
<style>
|
|
366
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
367
|
-
body {
|
|
368
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
369
|
-
background: #0a0a0a;
|
|
370
|
-
color: #fafafa;
|
|
371
|
-
display: flex;
|
|
372
|
-
align-items: center;
|
|
373
|
-
justify-content: center;
|
|
374
|
-
min-height: 100vh;
|
|
375
|
-
}
|
|
376
|
-
.card {
|
|
377
|
-
background: #171717;
|
|
378
|
-
border: 1px solid #262626;
|
|
379
|
-
border-radius: 16px;
|
|
380
|
-
padding: 48px;
|
|
381
|
-
max-width: 480px;
|
|
382
|
-
width: 100%;
|
|
383
|
-
text-align: center;
|
|
384
|
-
}
|
|
385
|
-
.icon {
|
|
386
|
-
width: 56px;
|
|
387
|
-
height: 56px;
|
|
388
|
-
background: #450a0a;
|
|
389
|
-
border-radius: 50%;
|
|
390
|
-
display: flex;
|
|
391
|
-
align-items: center;
|
|
392
|
-
justify-content: center;
|
|
393
|
-
margin: 0 auto 24px;
|
|
394
|
-
}
|
|
395
|
-
.icon svg { width: 28px; height: 28px; }
|
|
396
|
-
h1 { font-size: 22px; font-weight: 600; margin-bottom: 16px; color: #ef4444; }
|
|
397
|
-
.reason { font-size: 14px; color: #a3a3a3; line-height: 1.6; }
|
|
398
|
-
.reason a { color: #7c3aed; text-decoration: underline; }
|
|
399
|
-
</style>
|
|
400
|
-
</head>
|
|
401
|
-
<body>
|
|
402
|
-
<div class="card">
|
|
403
|
-
<div class="icon">
|
|
404
|
-
<svg fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2.5">
|
|
405
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
406
|
-
</svg>
|
|
407
|
-
</div>
|
|
408
|
-
<h1>Authorization Failed</h1>
|
|
409
|
-
<p class="reason">${reason}</p>
|
|
410
|
-
</div>
|
|
411
|
-
</body>
|
|
412
|
-
</html>`;
|
|
413
|
-
}
|
package/dist/auth/refresh.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JWT refresh logic for MCP server startup.
|
|
3
|
-
*
|
|
4
|
-
* On each MCP session start, we check if the access_token is expired
|
|
5
|
-
* and use the refresh_token to get a new one from Supabase.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Get a valid access token, refreshing if needed.
|
|
9
|
-
* Returns { access_token, user_id, email } or throws.
|
|
10
|
-
*/
|
|
11
|
-
export declare function getValidToken(): Promise<{
|
|
12
|
-
access_token: string;
|
|
13
|
-
refresh_token: string;
|
|
14
|
-
user_id: string;
|
|
15
|
-
email: string;
|
|
16
|
-
}>;
|
package/dist/auth/refresh.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JWT refresh logic for MCP server startup.
|
|
3
|
-
*
|
|
4
|
-
* On each MCP session start, we check if the access_token is expired
|
|
5
|
-
* and use the refresh_token to get a new one from Supabase.
|
|
6
|
-
*/
|
|
7
|
-
import { createClient } from "@supabase/supabase-js";
|
|
8
|
-
import { readCredentials, writeCredentials } from "./token-store.js";
|
|
9
|
-
import { ATOMS_SUPABASE_URL, ATOMS_SUPABASE_ANON_KEY } from "../config.js";
|
|
10
|
-
/**
|
|
11
|
-
* Get a valid access token, refreshing if needed.
|
|
12
|
-
* Returns { access_token, user_id, email } or throws.
|
|
13
|
-
*/
|
|
14
|
-
export async function getValidToken() {
|
|
15
|
-
// Priority 1: Environment variable (for CI/CD, headless)
|
|
16
|
-
const envToken = process.env.ATOMS_ACCESS_TOKEN;
|
|
17
|
-
if (envToken) {
|
|
18
|
-
const payload = decodeJwtPayload(envToken);
|
|
19
|
-
return {
|
|
20
|
-
access_token: envToken,
|
|
21
|
-
refresh_token: "",
|
|
22
|
-
user_id: payload.sub,
|
|
23
|
-
email: payload.email ?? "",
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
// Priority 2: Stored credentials
|
|
27
|
-
const creds = await readCredentials();
|
|
28
|
-
if (!creds) {
|
|
29
|
-
throw new Error("Not authenticated. Run 'npx @atoms-tech/atoms-mcp login' to connect your ATOMS account.");
|
|
30
|
-
}
|
|
31
|
-
// Check if token is still valid (with 60s buffer)
|
|
32
|
-
if (creds.expires_at > Date.now() + 60_000) {
|
|
33
|
-
const payload = decodeJwtPayload(creds.access_token);
|
|
34
|
-
return {
|
|
35
|
-
access_token: creds.access_token,
|
|
36
|
-
refresh_token: creds.refresh_token,
|
|
37
|
-
user_id: payload.sub,
|
|
38
|
-
email: payload.email ?? creds.user_email ?? "",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
// Token expired — refresh it
|
|
42
|
-
process.stderr.write("[atoms-mcp] Refreshing access token...\n");
|
|
43
|
-
const supabase = createClient(ATOMS_SUPABASE_URL, ATOMS_SUPABASE_ANON_KEY);
|
|
44
|
-
const { data, error } = await supabase.auth.refreshSession({
|
|
45
|
-
refresh_token: creds.refresh_token,
|
|
46
|
-
});
|
|
47
|
-
if (error || !data.session) {
|
|
48
|
-
throw new Error(`Token refresh failed: ${error?.message ?? "No session returned"}. ` +
|
|
49
|
-
"Re-run 'npx @atoms-tech/atoms-mcp login'.");
|
|
50
|
-
}
|
|
51
|
-
const session = data.session;
|
|
52
|
-
// Update stored credentials
|
|
53
|
-
await writeCredentials({
|
|
54
|
-
access_token: session.access_token,
|
|
55
|
-
refresh_token: session.refresh_token,
|
|
56
|
-
expires_at: Date.now() + (session.expires_in ?? 3600) * 1000,
|
|
57
|
-
user_email: session.user?.email,
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
access_token: session.access_token,
|
|
61
|
-
refresh_token: session.refresh_token,
|
|
62
|
-
user_id: session.user?.id ?? "",
|
|
63
|
-
email: session.user?.email ?? "",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Decode JWT payload without verification (Supabase already verified it).
|
|
68
|
-
*/
|
|
69
|
-
function decodeJwtPayload(jwt) {
|
|
70
|
-
const parts = jwt.split(".");
|
|
71
|
-
if (parts.length !== 3)
|
|
72
|
-
throw new Error("Invalid JWT format");
|
|
73
|
-
const padded = parts[1]
|
|
74
|
-
.replace(/-/g, "+")
|
|
75
|
-
.replace(/_/g, "/")
|
|
76
|
-
.padEnd(parts[1].length + ((4 - (parts[1].length % 4)) % 4), "=");
|
|
77
|
-
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
|
|
78
|
-
if (!payload.sub)
|
|
79
|
-
throw new Error("JWT missing 'sub' claim");
|
|
80
|
-
return payload;
|
|
81
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token storage for MCP auth credentials.
|
|
3
|
-
*
|
|
4
|
-
* Stores access_token + refresh_token in ~/.atoms/credentials.json
|
|
5
|
-
* with file permissions 0600 (owner read/write only).
|
|
6
|
-
*/
|
|
7
|
-
export interface StoredCredentials {
|
|
8
|
-
access_token: string;
|
|
9
|
-
refresh_token: string;
|
|
10
|
-
expires_at: number;
|
|
11
|
-
user_email?: string;
|
|
12
|
-
supabase_url?: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Read stored credentials. Returns null if not found or invalid.
|
|
16
|
-
*/
|
|
17
|
-
export declare function readCredentials(): Promise<StoredCredentials | null>;
|
|
18
|
-
/**
|
|
19
|
-
* Write credentials to disk with secure permissions.
|
|
20
|
-
*/
|
|
21
|
-
export declare function writeCredentials(creds: StoredCredentials): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Check if stored credentials exist and are not expired.
|
|
24
|
-
*/
|
|
25
|
-
export declare function hasValidCredentials(): Promise<boolean>;
|
|
26
|
-
/**
|
|
27
|
-
* Delete stored credentials (logout).
|
|
28
|
-
*/
|
|
29
|
-
export declare function clearCredentials(): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Get the credentials file path (for user-facing messages).
|
|
32
|
-
*/
|
|
33
|
-
export declare function getCredentialsPath(): string;
|
package/dist/auth/token-store.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token storage for MCP auth credentials.
|
|
3
|
-
*
|
|
4
|
-
* Stores access_token + refresh_token in ~/.atoms/credentials.json
|
|
5
|
-
* with file permissions 0600 (owner read/write only).
|
|
6
|
-
*/
|
|
7
|
-
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
const ATOMS_DIR = join(homedir(), ".atoms");
|
|
11
|
-
const CREDENTIALS_FILE = join(ATOMS_DIR, "credentials.json");
|
|
12
|
-
/**
|
|
13
|
-
* Read stored credentials. Returns null if not found or invalid.
|
|
14
|
-
*/
|
|
15
|
-
export async function readCredentials() {
|
|
16
|
-
try {
|
|
17
|
-
const content = await readFile(CREDENTIALS_FILE, "utf-8");
|
|
18
|
-
const creds = JSON.parse(content);
|
|
19
|
-
if (!creds.access_token || !creds.refresh_token)
|
|
20
|
-
return null;
|
|
21
|
-
return creds;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Write credentials to disk with secure permissions.
|
|
29
|
-
*/
|
|
30
|
-
export async function writeCredentials(creds) {
|
|
31
|
-
await mkdir(ATOMS_DIR, { recursive: true, mode: 0o700 });
|
|
32
|
-
await writeFile(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Check if stored credentials exist and are not expired.
|
|
36
|
-
*/
|
|
37
|
-
export async function hasValidCredentials() {
|
|
38
|
-
const creds = await readCredentials();
|
|
39
|
-
if (!creds)
|
|
40
|
-
return false;
|
|
41
|
-
// Consider expired if less than 60 seconds remaining
|
|
42
|
-
return creds.expires_at > Date.now() + 60_000;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Delete stored credentials (logout).
|
|
46
|
-
*/
|
|
47
|
-
export async function clearCredentials() {
|
|
48
|
-
try {
|
|
49
|
-
await unlink(CREDENTIALS_FILE);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// File doesn't exist — already logged out
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Get the credentials file path (for user-facing messages).
|
|
57
|
-
*/
|
|
58
|
-
export function getCredentialsPath() {
|
|
59
|
-
return CREDENTIALS_FILE;
|
|
60
|
-
}
|
package/dist/config.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ATOMS MCP Server — Public client configuration.
|
|
3
|
-
*
|
|
4
|
-
* These are PUBLISHABLE values — identical to the ones embedded in the
|
|
5
|
-
* ATOMS.tech React frontend bundle (NEXT_PUBLIC_SUPABASE_*).
|
|
6
|
-
*
|
|
7
|
-
* The anon key is NOT a secret. It only grants access to Supabase's API
|
|
8
|
-
* gateway. All data access is gated by Row Level Security (RLS) policies
|
|
9
|
-
* which require a valid user JWT. Without authentication, these values
|
|
10
|
-
* grant zero data access.
|
|
11
|
-
*
|
|
12
|
-
* This is the standard pattern used by Supabase, Firebase, and Clerk
|
|
13
|
-
* for client-side applications.
|
|
14
|
-
*/
|
|
15
|
-
export declare const ATOMS_SUPABASE_URL = "https://gmebjyhomsbvhrxffzre.supabase.co";
|
|
16
|
-
export declare const ATOMS_SUPABASE_ANON_KEY = "sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH";
|
|
17
|
-
/** ATOMS web app URL — hosts the MCP consent page */
|
|
18
|
-
export declare const ATOMS_APP_URL: string;
|
package/dist/config.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ATOMS MCP Server — Public client configuration.
|
|
3
|
-
*
|
|
4
|
-
* These are PUBLISHABLE values — identical to the ones embedded in the
|
|
5
|
-
* ATOMS.tech React frontend bundle (NEXT_PUBLIC_SUPABASE_*).
|
|
6
|
-
*
|
|
7
|
-
* The anon key is NOT a secret. It only grants access to Supabase's API
|
|
8
|
-
* gateway. All data access is gated by Row Level Security (RLS) policies
|
|
9
|
-
* which require a valid user JWT. Without authentication, these values
|
|
10
|
-
* grant zero data access.
|
|
11
|
-
*
|
|
12
|
-
* This is the standard pattern used by Supabase, Firebase, and Clerk
|
|
13
|
-
* for client-side applications.
|
|
14
|
-
*/
|
|
15
|
-
export const ATOMS_SUPABASE_URL = "https://gmebjyhomsbvhrxffzre.supabase.co";
|
|
16
|
-
export const ATOMS_SUPABASE_ANON_KEY = "sb_publishable_b49MUAB8XCrQiF7x5b9lUA_w1aplSBH";
|
|
17
|
-
/** ATOMS web app URL — hosts the MCP consent page */
|
|
18
|
-
export const ATOMS_APP_URL = process.env.ATOMS_APP_URL ?? "https://x.atoms.tech";
|
package/dist/db/client.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supabase client factory for the MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Creates a user-scoped client with JWT — RLS enforced automatically.
|
|
5
|
-
* Never uses service role key (all queries respect user permissions).
|
|
6
|
-
*/
|
|
7
|
-
import { type SupabaseClient } from "@supabase/supabase-js";
|
|
8
|
-
/**
|
|
9
|
-
* Get or create an authenticated Supabase client.
|
|
10
|
-
* Auto-refreshes JWT when expired. All queries run with user's RLS permissions.
|
|
11
|
-
*/
|
|
12
|
-
export declare function getClient(): Promise<SupabaseClient>;
|
|
13
|
-
/**
|
|
14
|
-
* Get the authenticated user's ID. Must call getClient() first.
|
|
15
|
-
*/
|
|
16
|
-
export declare function getUserId(): string;
|
|
17
|
-
/**
|
|
18
|
-
* Get the authenticated user's email.
|
|
19
|
-
*/
|
|
20
|
-
export declare function getUserEmail(): string;
|
|
21
|
-
/**
|
|
22
|
-
* Reset the cached client (for token refresh).
|
|
23
|
-
*/
|
|
24
|
-
export declare function resetClient(): void;
|
|
25
|
-
/**
|
|
26
|
-
* Check if the user has write access to a project.
|
|
27
|
-
* Returns the user's org role, or throws a descriptive error.
|
|
28
|
-
*/
|
|
29
|
-
export declare function requireWriteAccess(client: SupabaseClient, projectId: string): Promise<string>;
|