vana-cli 0.34.0 → 0.35.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/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/personal-server/local/runtime-pkg/entry.mjs +12 -1
- package/dist/personal-server/local/runtime-pkg/mcp-approval.mjs +316 -0
- package/dist/personal-server/local/runtime.d.ts.map +1 -1
- package/dist/personal-server/local/runtime.js +5 -1
- package/dist/personal-server/local/runtime.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@opendatalabs/personal-server-ts/node";
|
|
23
23
|
|
|
24
24
|
import { applyDerived, derivedConfig } from "./derived-config.mjs";
|
|
25
|
+
import { startMcpApprovalPage } from "./mcp-approval.mjs";
|
|
25
26
|
|
|
26
27
|
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
27
28
|
|
|
@@ -38,6 +39,7 @@ async function readInput() {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
let ps = null;
|
|
42
|
+
let mcpApproval = null;
|
|
41
43
|
|
|
42
44
|
// Only a tunnel URL may be registered: without one the server offers its
|
|
43
45
|
// localhost origin, which no app could ever reach.
|
|
@@ -171,12 +173,20 @@ async function main() {
|
|
|
171
173
|
configDefaults[key] = applyDerived(loaded[key], derived[key]);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
// Where the server sends the owner to approve an MCP client (claude.ai's
|
|
177
|
+
// connector): a page this process serves on 127.0.0.1, not Vana Desktop's
|
|
178
|
+
// vana:// link, which dead-ends on a machine without Desktop.
|
|
179
|
+
mcpApproval = await startMcpApprovalPage({
|
|
180
|
+
serverOrigin: () => `http://127.0.0.1:${port}`,
|
|
181
|
+
accessToken: process.env.PS_ACCESS_TOKEN ?? "",
|
|
182
|
+
});
|
|
183
|
+
|
|
174
184
|
ps = await startPersonalServer({
|
|
175
185
|
configPath,
|
|
176
186
|
rootPath,
|
|
177
187
|
port,
|
|
178
188
|
ownerSignature,
|
|
179
|
-
mcpOAuthApprovalUrl:
|
|
189
|
+
mcpOAuthApprovalUrl: mcpApproval.url,
|
|
180
190
|
configDefaults,
|
|
181
191
|
onStatus: (status) => process.stderr.write(`[status] ${status}\n`),
|
|
182
192
|
});
|
|
@@ -197,6 +207,7 @@ async function main() {
|
|
|
197
207
|
|
|
198
208
|
async function shutdown(signal) {
|
|
199
209
|
process.stderr.write(`[entry] ${signal}, stopping\n`);
|
|
210
|
+
mcpApproval?.close();
|
|
200
211
|
try {
|
|
201
212
|
await ps?.stop();
|
|
202
213
|
} finally {
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The page where the owner approves an MCP client (Claude on claude.ai, any
|
|
2
|
+
// remote MCP app) that asked the Personal Server for access over OAuth.
|
|
3
|
+
//
|
|
4
|
+
// The server's /mcp/oauth/authorize redirects the owner's browser here with
|
|
5
|
+
// ?mcp_authorization=<id>. This page shows who is asking, lets the owner pick
|
|
6
|
+
// which data it may read, and approves through the server's owner-only API
|
|
7
|
+
// with the owner token only this process holds. Loopback only: the approval
|
|
8
|
+
// happens on the machine that runs the server.
|
|
9
|
+
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import http from "node:http";
|
|
12
|
+
|
|
13
|
+
const APPROVAL_PATH = "/mcp";
|
|
14
|
+
|
|
15
|
+
const escapeHtml = (value) =>
|
|
16
|
+
String(value).replace(
|
|
17
|
+
/[&<>"']/g,
|
|
18
|
+
(ch) =>
|
|
19
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
20
|
+
ch
|
|
21
|
+
],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** "Claude" for claude.ai, else the redirect's host: who is asking. */
|
|
25
|
+
export function requesterName(redirectUri) {
|
|
26
|
+
let host = "";
|
|
27
|
+
try {
|
|
28
|
+
host = new URL(redirectUri).host;
|
|
29
|
+
} catch {
|
|
30
|
+
return "An app";
|
|
31
|
+
}
|
|
32
|
+
if (/(^|\.)claude\.(ai|com)$/.test(host)) return "Claude";
|
|
33
|
+
if (/(^|\.)chatgpt\.com$|(^|\.)openai\.com$/.test(host)) return "ChatGPT";
|
|
34
|
+
return host || "An app";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The Vana logotype, as the server's own 1.25 approval pages draw it.
|
|
38
|
+
const LOGO = `<svg aria-label="Vana" role="img" viewBox="0 0 718 200" height="13" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M344.76 99.9947C344.76 129.81 344.76 159.639 344.76 189.454C344.76 191.122 344.786 192.804 344.548 194.446C344.004 198.236 342.161 199.864 338.343 199.891C330.162 199.944 321.982 199.918 313.788 199.918C291.965 199.918 270.141 199.958 248.318 199.904C220.541 199.824 195.417 179.511 189.662 152.152C185.287 131.318 189.967 112.527 204.207 96.6715C211.446 88.6104 221.045 84.0193 231.453 81.31C243.306 78.2137 255.424 78.6008 267.503 78.6408C274.265 78.6675 281.026 78.6408 287.788 78.6408C294.987 78.6408 300.662 75.8782 304.255 69.3786C307.888 62.7989 307.371 56.2726 303.672 49.9999C301.312 45.9961 297.546 43.7139 293.012 42.9798C290.214 42.5261 287.351 42.3526 284.513 42.3392C264.228 42.2858 243.956 42.3259 223.67 42.3125C216.047 42.3125 213.727 39.9502 213.713 32.2362C213.7 24.3619 213.673 16.4877 213.726 8.61342C213.78 2.32737 215.848 0.325439 222.106 0.325439C259.945 0.325439 297.772 0.325439 335.611 0.325439C343.222 0.325439 344.773 1.83356 344.773 9.45423C344.786 39.6299 344.773 69.8056 344.773 99.9947H344.76ZM268.709 157.944C275.471 157.944 282.22 157.957 288.981 157.944C299.363 157.904 306.642 150.603 306.748 140.153C306.867 129.209 299.734 121.696 289.034 121.642C275.643 121.575 262.239 121.575 248.848 121.642C238.732 121.682 232.196 127.328 231.201 137.431C230.114 148.588 236.147 157.971 248.782 157.957C255.424 157.957 262.054 157.957 268.696 157.957L268.709 157.944Z"/><path d="M717.957 100.395C717.957 130.571 717.957 160.76 717.957 190.936C717.957 198.743 716.791 199.931 709.207 199.931C680.264 199.931 651.321 199.717 622.391 199.998C595.489 200.251 568.058 180.325 562.741 149.936C559.32 130.411 563.616 112.38 577.113 97.3522C586.42 86.9955 598.539 81.8706 612.076 79.922C628.476 77.5598 644.996 78.9878 661.45 78.6809C675.332 78.4273 683.87 65.5749 678.5 52.6558C676.286 47.3307 672.136 44.181 666.568 42.9531C664.009 42.3792 661.397 42.3125 658.785 42.3125C638.38 42.3259 617.989 42.3259 597.584 42.3125C589.364 42.3125 587.362 40.2572 587.349 31.9025C587.349 24.0283 587.322 16.154 587.349 8.27976C587.375 2.14052 589.178 0.325439 595.145 0.325439C633.461 0.325439 671.765 0.325439 710.082 0.325439C716.473 0.325439 717.957 1.83356 717.957 8.41322C717.957 39.0694 717.957 69.7256 717.957 100.382V100.395ZM642.398 121.616C636.113 121.616 629.815 121.963 623.571 121.522C612.699 120.748 603.709 128.943 604.611 141.181C605.314 150.777 611.916 157.85 621.436 157.904C635.291 157.984 649.159 157.984 663.014 157.904C673.33 157.85 679.866 150.79 679.879 139.913C679.879 128.702 673.555 121.776 663.014 121.616C656.147 121.522 649.279 121.602 642.398 121.602V121.616Z"/><path d="M379.576 99.9952C379.576 70.1798 379.576 40.3645 379.576 10.5491C379.576 8.88082 379.576 7.1992 379.828 5.55762C380.358 2.15435 382.294 0.312575 385.887 0.325921C413.04 0.365959 440.247 -0.608312 467.347 0.632883C500.546 2.15435 527.5 23.4282 535.124 57.2741C536.94 65.3352 537.763 73.4496 537.749 81.7109C537.683 117.852 537.723 153.98 537.709 190.122C537.709 191.67 537.802 193.258 537.511 194.753C536.901 197.903 535.071 199.891 531.664 199.891C521.349 199.905 511.033 199.905 500.718 199.891C497.656 199.891 495.813 198.196 495.269 195.274C494.924 193.418 494.885 191.483 494.885 189.588C494.858 152.98 494.898 116.358 494.858 79.749C494.832 62.1187 484.742 47.7849 468.527 43.7143C454.526 40.2043 438.231 43.1805 428.844 56.1263C424.588 61.9986 422.52 68.6183 422.507 75.8519C422.48 113.902 422.494 151.939 422.48 189.988C422.48 191.417 422.547 192.858 422.374 194.273C421.937 197.836 419.776 199.865 416.222 199.891C406.027 199.945 395.831 199.905 385.622 199.918C382.387 199.918 380.491 198.223 379.934 195.14C379.616 193.405 379.589 191.59 379.589 189.802C379.576 159.866 379.576 129.931 379.576 99.9952Z"/><path d="M89.4414 199.918C73.0805 199.918 56.7064 199.918 40.3454 199.918C34.4719 199.918 33.0798 198.891 32.0059 192.978C29.7785 180.673 27.7632 168.341 25.6153 156.023C22.4863 138.072 19.3043 120.135 16.2018 102.184C13.6032 87.1697 11.0973 72.1419 8.51192 57.1141C5.83371 41.7526 3.11573 26.3912 0.450787 11.0297C0.17236 9.38813 0 7.70652 0 6.05159C0.0265169 2.63497 1.78989 0.472886 5.13102 0.432848C15.9234 0.28604 26.7158 0.339424 37.5081 0.406155C41.1012 0.432848 42.2944 3.20885 42.9308 6.07828C44.0313 11.0698 44.9329 16.1146 45.8212 21.1595C48.0751 33.9451 50.276 46.7307 52.5034 59.5164C56.0699 79.9227 59.61 100.329 63.2163 120.735C64.2902 126.834 65.4304 132.92 66.69 138.98C68.9837 150.03 78.0127 157.557 89.163 157.864C99.8493 158.158 109.674 150.978 112.458 140.408C115.136 130.264 116.343 119.828 118.212 109.511C121.487 91.347 124.669 73.1695 127.851 54.992C130.384 40.4447 132.85 25.8974 135.342 11.35C135.448 10.7628 135.488 10.1622 135.594 9.57498C137.026 1.22027 138.087 0.312732 146.4 0.312732C155.057 0.312732 163.715 0.28604 172.373 0.326078C177.769 0.352771 179.983 2.54154 179.175 7.79994C177.159 20.8525 174.879 33.8784 172.651 46.9042C169.575 64.8549 166.46 82.8055 163.344 100.756C160.719 115.891 158.041 131.025 155.415 146.16C152.804 161.174 150.218 176.189 147.619 191.203C147.474 192.031 147.341 192.845 147.169 193.659C146.121 198.517 144.53 199.878 139.625 199.878C122.906 199.905 106.187 199.878 89.4547 199.878L89.4414 199.918Z"/></svg>`;
|
|
39
|
+
|
|
40
|
+
// The Vana Account look (same tokens as the server's own approval pages).
|
|
41
|
+
const STYLE = `
|
|
42
|
+
:root{color-scheme:light dark;--canvas:#f5f5f5;--panel:#fff;--text:#1a1918;--dim:#736c64;--accent:#4141fc;--error:#e7000b;--line:#c2c2c2}
|
|
43
|
+
@media (prefers-color-scheme:dark){:root{--canvas:#121212;--panel:#1b1c1d;--text:#d1d1d1;--dim:#a4a4a4;--accent:#559bec;--error:#f75e54;--line:#3d3d3d}}
|
|
44
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--canvas);color:var(--text);font:15px/1.5 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:16px}
|
|
45
|
+
main{width:100%;max-width:480px;background:var(--panel);border-radius:14px;outline:1px solid color-mix(in srgb,var(--line) 20%,transparent);padding:32px}
|
|
46
|
+
.logo{color:var(--accent);margin-bottom:12px}h1{font-size:30px;font-weight:500;line-height:1.05;letter-spacing:-.01em;margin:0 0 12px}h1 .kicker{color:var(--accent);display:block}
|
|
47
|
+
p{color:var(--dim);margin:0 0 16px}ul{list-style:none;padding:0;margin:0 0 20px;max-height:280px;overflow:auto}li{padding:6px 0}label{display:flex;gap:10px;align-items:center;font-family:"IBM Plex Mono",ui-monospace,Menlo,monospace;font-size:14px}
|
|
48
|
+
button{width:100%;min-height:44px;border:0;border-radius:999px;font:inherit;cursor:pointer;margin-top:8px}.primary{background:var(--text);color:var(--panel)}.secondary{background:transparent;color:var(--dim);outline:1px solid color-mix(in srgb,var(--line) 60%,transparent)}
|
|
49
|
+
.error{color:var(--error)}code{font-family:"IBM Plex Mono",ui-monospace,Menlo,monospace;font-size:13px}
|
|
50
|
+
@media (max-width:480px){main{padding:20px}h1{font-size:27px}}`;
|
|
51
|
+
|
|
52
|
+
function page(title, body) {
|
|
53
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${STYLE}</style></head><body><main><div class="logo">${LOGO}</div>${body}</main></body></html>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function messagePage(heading, text, isError = false) {
|
|
57
|
+
return page(
|
|
58
|
+
heading,
|
|
59
|
+
`<h1><span class="kicker">Personal Server</span>${escapeHtml(heading)}</h1><p${isError ? ' class="error"' : ""}>${escapeHtml(text)}</p>`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Serve the approval page on 127.0.0.1 and return its URL, which the server
|
|
65
|
+
* uses as its MCP OAuth approval URL.
|
|
66
|
+
*
|
|
67
|
+
* @param {{ serverOrigin: () => string, accessToken: string }} input
|
|
68
|
+
*/
|
|
69
|
+
export async function startMcpApprovalPage(input) {
|
|
70
|
+
const pending = new Map(); // authorization id -> one-time form token
|
|
71
|
+
|
|
72
|
+
const ownerFetch = (path, init = {}) =>
|
|
73
|
+
fetch(`${input.serverOrigin()}${path}`, {
|
|
74
|
+
...init,
|
|
75
|
+
headers: {
|
|
76
|
+
...(init.headers ?? {}),
|
|
77
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
78
|
+
"content-type": "application/json",
|
|
79
|
+
},
|
|
80
|
+
signal: AbortSignal.timeout(30_000),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
async function listScopes() {
|
|
84
|
+
const scopes = [];
|
|
85
|
+
for (let offset = 0; offset < 10_000; offset += 100) {
|
|
86
|
+
const response = await ownerFetch(`/v1/data?limit=100&offset=${offset}`);
|
|
87
|
+
if (!response.ok) break;
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
const batch = Array.isArray(body.scopes) ? body.scopes : [];
|
|
90
|
+
for (const item of batch) {
|
|
91
|
+
const scope = typeof item === "string" ? item : item?.scope;
|
|
92
|
+
if (typeof scope === "string" && !scopes.includes(scope))
|
|
93
|
+
scopes.push(scope);
|
|
94
|
+
}
|
|
95
|
+
const total = typeof body.total === "number" ? body.total : null;
|
|
96
|
+
if (batch.length === 0 || total === null || scopes.length >= total) break;
|
|
97
|
+
}
|
|
98
|
+
return scopes.sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let listenerPort = 0;
|
|
102
|
+
const allowedHosts = () => [
|
|
103
|
+
`127.0.0.1:${listenerPort}`,
|
|
104
|
+
`localhost:${listenerPort}`,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const server = http.createServer(async (req, res) => {
|
|
108
|
+
const send = (status, html, headers = {}) => {
|
|
109
|
+
res.writeHead(status, {
|
|
110
|
+
"content-type": "text/html; charset=utf-8",
|
|
111
|
+
"cache-control": "no-store",
|
|
112
|
+
"x-frame-options": "DENY",
|
|
113
|
+
"content-security-policy":
|
|
114
|
+
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; form-action 'self' https: http:; frame-ancestors 'none'",
|
|
115
|
+
...headers,
|
|
116
|
+
});
|
|
117
|
+
res.end(html);
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
// A page elsewhere must not drive this through a rebound DNS name.
|
|
121
|
+
if (!allowedHosts().includes(req.headers.host ?? "")) {
|
|
122
|
+
return send(
|
|
123
|
+
400,
|
|
124
|
+
messagePage(
|
|
125
|
+
"Wrong address",
|
|
126
|
+
"Open this page from the link your app gave you.",
|
|
127
|
+
true,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
132
|
+
if (url.pathname !== APPROVAL_PATH)
|
|
133
|
+
return send(404, messagePage("Not found", "Nothing here.", true));
|
|
134
|
+
|
|
135
|
+
if (req.method === "GET") {
|
|
136
|
+
const id = url.searchParams.get("mcp_authorization") ?? "";
|
|
137
|
+
const response = await ownerFetch(
|
|
138
|
+
`/v1/mcp/oauth/authorizations/${encodeURIComponent(id)}`,
|
|
139
|
+
);
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
return send(
|
|
142
|
+
404,
|
|
143
|
+
messagePage(
|
|
144
|
+
"Request not found",
|
|
145
|
+
"This request expired or was already answered. Start the connection again from your app.",
|
|
146
|
+
true,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const authorization = await response.json();
|
|
151
|
+
if (authorization.status !== "pending") {
|
|
152
|
+
return send(
|
|
153
|
+
409,
|
|
154
|
+
messagePage(
|
|
155
|
+
"Already answered",
|
|
156
|
+
"This request was already approved or has expired. Start the connection again from your app if you need to.",
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const who = requesterName(authorization.redirectUri);
|
|
161
|
+
const scopes = await listScopes();
|
|
162
|
+
const token = crypto.randomBytes(24).toString("base64url");
|
|
163
|
+
pending.set(id, token);
|
|
164
|
+
const list = scopes.length
|
|
165
|
+
? `<ul>${scopes
|
|
166
|
+
.map(
|
|
167
|
+
(scope) =>
|
|
168
|
+
`<li><label><input type="checkbox" name="scope" value="${escapeHtml(scope)}" checked> ${escapeHtml(scope)}</label></li>`,
|
|
169
|
+
)
|
|
170
|
+
.join("")}</ul>`
|
|
171
|
+
: `<p class="error">Your Personal Server holds no data yet. Collect some with <code>vana connect <source></code> first.</p>`;
|
|
172
|
+
return send(
|
|
173
|
+
200,
|
|
174
|
+
page(
|
|
175
|
+
`Allow ${who}`,
|
|
176
|
+
`<h1><span class="kicker">Personal Server</span>Allow ${escapeHtml(who)} to read your data</h1>
|
|
177
|
+
<p>${escapeHtml(who)} (${escapeHtml(new URL(authorization.redirectUri).host)}) asks to read data from your Personal Server. It can read only what you tick, and you can revoke it any time.</p>
|
|
178
|
+
<form method="post" action="${APPROVAL_PATH}">
|
|
179
|
+
<input type="hidden" name="id" value="${escapeHtml(id)}"><input type="hidden" name="token" value="${escapeHtml(token)}">
|
|
180
|
+
${list}
|
|
181
|
+
<button class="primary" name="action" value="approve"${scopes.length ? "" : " disabled"}>Approve</button>
|
|
182
|
+
<button class="secondary" name="action" value="deny">Deny</button>
|
|
183
|
+
</form>`,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (req.method === "POST") {
|
|
189
|
+
const origin = req.headers.origin;
|
|
190
|
+
if (
|
|
191
|
+
origin &&
|
|
192
|
+
!allowedHosts()
|
|
193
|
+
.map((h) => `http://${h}`)
|
|
194
|
+
.includes(origin)
|
|
195
|
+
) {
|
|
196
|
+
return send(
|
|
197
|
+
403,
|
|
198
|
+
messagePage(
|
|
199
|
+
"Not allowed",
|
|
200
|
+
"This form can only be sent from its own page.",
|
|
201
|
+
true,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
let raw = "";
|
|
206
|
+
for await (const chunk of req) {
|
|
207
|
+
raw += chunk;
|
|
208
|
+
if (raw.length > 100_000)
|
|
209
|
+
return send(
|
|
210
|
+
413,
|
|
211
|
+
messagePage("Too large", "Request too large.", true),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const form = new URLSearchParams(raw);
|
|
215
|
+
const id = form.get("id") ?? "";
|
|
216
|
+
const token = form.get("token") ?? "";
|
|
217
|
+
const expected = pending.get(id);
|
|
218
|
+
if (
|
|
219
|
+
!expected ||
|
|
220
|
+
token.length !== expected.length ||
|
|
221
|
+
!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected))
|
|
222
|
+
) {
|
|
223
|
+
return send(
|
|
224
|
+
403,
|
|
225
|
+
messagePage(
|
|
226
|
+
"Request expired",
|
|
227
|
+
"Reload the page from your app and try again.",
|
|
228
|
+
true,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
pending.delete(id);
|
|
233
|
+
|
|
234
|
+
const details = await ownerFetch(
|
|
235
|
+
`/v1/mcp/oauth/authorizations/${encodeURIComponent(id)}`,
|
|
236
|
+
);
|
|
237
|
+
const authorization = details.ok ? await details.json() : null;
|
|
238
|
+
if (form.get("action") === "deny") {
|
|
239
|
+
if (!authorization?.redirectUri)
|
|
240
|
+
return send(
|
|
241
|
+
200,
|
|
242
|
+
messagePage(
|
|
243
|
+
"Denied",
|
|
244
|
+
"Nothing was shared. You can close this tab.",
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
const back = new URL(authorization.redirectUri);
|
|
248
|
+
back.searchParams.set("error", "access_denied");
|
|
249
|
+
back.searchParams.set("error_description", "The owner denied access");
|
|
250
|
+
if (authorization.state)
|
|
251
|
+
back.searchParams.set("state", authorization.state);
|
|
252
|
+
res.writeHead(303, {
|
|
253
|
+
location: back.toString(),
|
|
254
|
+
"cache-control": "no-store",
|
|
255
|
+
});
|
|
256
|
+
return res.end();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const scopes = form.getAll("scope").filter(Boolean);
|
|
260
|
+
if (scopes.length === 0) {
|
|
261
|
+
return send(
|
|
262
|
+
400,
|
|
263
|
+
messagePage(
|
|
264
|
+
"Nothing selected",
|
|
265
|
+
"Tick at least one kind of data, or deny. Go back and try again.",
|
|
266
|
+
true,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const approved = await ownerFetch(
|
|
271
|
+
`/v1/mcp/oauth/authorizations/${encodeURIComponent(id)}/approve`,
|
|
272
|
+
{
|
|
273
|
+
method: "POST",
|
|
274
|
+
body: JSON.stringify({ scopes }),
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
const body = await approved.json().catch(() => ({}));
|
|
278
|
+
if (!approved.ok || typeof body.redirectTo !== "string") {
|
|
279
|
+
const reason =
|
|
280
|
+
body?.error?.message ?? body?.message ?? `HTTP ${approved.status}`;
|
|
281
|
+
return send(
|
|
282
|
+
502,
|
|
283
|
+
messagePage(
|
|
284
|
+
"Approval failed",
|
|
285
|
+
`Your Personal Server could not approve this: ${reason}`,
|
|
286
|
+
true,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
res.writeHead(303, {
|
|
291
|
+
location: body.redirectTo,
|
|
292
|
+
"cache-control": "no-store",
|
|
293
|
+
});
|
|
294
|
+
return res.end();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return send(405, messagePage("Not allowed", "Method not allowed.", true));
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return send(
|
|
300
|
+
500,
|
|
301
|
+
messagePage(
|
|
302
|
+
"Something went wrong",
|
|
303
|
+
error instanceof Error ? error.message : String(error),
|
|
304
|
+
true,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
311
|
+
listenerPort = server.address().port;
|
|
312
|
+
return {
|
|
313
|
+
url: `http://127.0.0.1:${listenerPort}${APPROVAL_PATH}`,
|
|
314
|
+
close: () => server.close(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/personal-server/local/runtime.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/personal-server/local/runtime.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAuCvD,wBAAgB,kBAAkB,CAAC,GAAG,SAA0B,GAAG,OAAO,CAYzE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,SAA0B,GAC5B,OAAO,CAAC,MAAM,CAAC,CAwCjB"}
|
|
@@ -8,7 +8,11 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import { localServerRuntimeDir } from "./config.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
/** The scripts the server process runs; they change with the CLI, not the pin. */
|
|
11
|
-
const SCRIPT_FILES = [
|
|
11
|
+
const SCRIPT_FILES = [
|
|
12
|
+
"entry.mjs",
|
|
13
|
+
"derived-config.mjs",
|
|
14
|
+
"mcp-approval.mjs",
|
|
15
|
+
];
|
|
12
16
|
/** Files shipped with the CLI that define the runtime: the pin, its lock, the scripts. */
|
|
13
17
|
const ASSET_FILES = [
|
|
14
18
|
"package.json",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/personal-server/local/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,kFAAkF;AAClF,MAAM,YAAY,GAAG,
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/personal-server/local/runtime.ts"],"names":[],"mappings":"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"}
|