@cg3/prior-mcp 0.6.3 → 0.7.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/README.md +145 -129
- package/dist/client.d.ts +53 -6
- package/dist/client.js +514 -24
- package/dist/index.d.ts +4 -6
- package/dist/index.js +33 -13
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +227 -221
- package/dist/tools.js +14 -9
- package/package.json +4 -1
package/dist/client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Prior API client
|
|
3
|
+
* Prior API client shared between local MCP (stdio) and remote MCP server.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - API keys for durable machine auth
|
|
7
|
+
* - first-party OIDC browser login for interactive human auth
|
|
7
8
|
*/
|
|
8
9
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
10
|
if (k2 === undefined) k2 = k;
|
|
@@ -39,16 +40,159 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
40
|
};
|
|
40
41
|
})();
|
|
41
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.PriorApiClient = exports.CONFIG_PATH = void 0;
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
43
|
+
exports.PriorApiClient = exports.OIDC_CLIENT_ID = exports.CONFIG_PATH = void 0;
|
|
44
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
45
|
+
const crypto = __importStar(require("node:crypto"));
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const http = __importStar(require("node:http"));
|
|
48
|
+
const os = __importStar(require("node:os"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
46
50
|
exports.CONFIG_PATH = path.join(os.homedir(), ".prior", "config.json");
|
|
47
|
-
|
|
51
|
+
exports.OIDC_CLIENT_ID = "prior-mcp";
|
|
52
|
+
const VERSION = "0.7.0";
|
|
53
|
+
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
54
|
+
// Renamed prior:read/prior:write → account:read/account:write on 2026-04-26 per
|
|
55
|
+
// the oauth-scope-namespace-overhaul initiative (operations/initiatives/...).
|
|
56
|
+
// Breaking change for callers using DEFAULT_OIDC_SCOPES; bump npm minor.
|
|
57
|
+
const DEFAULT_OIDC_SCOPES = "openid profile email account:read account:write offline_access";
|
|
58
|
+
function compactConfig(config) {
|
|
59
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
60
|
+
}
|
|
61
|
+
function escapeHtml(input) {
|
|
62
|
+
return input
|
|
63
|
+
.replace(/&/g, "&")
|
|
64
|
+
.replace(/</g, "<")
|
|
65
|
+
.replace(/>/g, ">")
|
|
66
|
+
.replace(/"/g, """)
|
|
67
|
+
.replace(/'/g, "'");
|
|
68
|
+
}
|
|
69
|
+
function decodeJwtPayload(token) {
|
|
70
|
+
const parts = token.split(".");
|
|
71
|
+
if (parts.length !== 3)
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const padded = parts[1] + "=".repeat((4 - (parts[1].length % 4)) % 4);
|
|
75
|
+
return JSON.parse(Buffer.from(padded, "base64url").toString("utf-8"));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function deriveExpiresAt(accessToken, expiresIn) {
|
|
82
|
+
if (typeof expiresIn === "number" && expiresIn > 0) {
|
|
83
|
+
return new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
84
|
+
}
|
|
85
|
+
const payload = decodeJwtPayload(accessToken);
|
|
86
|
+
if (typeof payload?.exp === "number") {
|
|
87
|
+
return new Date(payload.exp * 1000).toISOString();
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function isExpired(expiresAt, accessToken) {
|
|
92
|
+
if (expiresAt) {
|
|
93
|
+
const expiryMs = Date.parse(expiresAt);
|
|
94
|
+
if (!Number.isNaN(expiryMs)) {
|
|
95
|
+
return expiryMs <= (Date.now() + 30_000);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
|
|
99
|
+
if (typeof payload?.exp === "number") {
|
|
100
|
+
return (payload.exp * 1000) <= (Date.now() + 30_000);
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
function launchDetached(command, args) {
|
|
105
|
+
const child = childProcess.spawn(command, args, {
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: "ignore",
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
});
|
|
110
|
+
child.unref();
|
|
111
|
+
}
|
|
112
|
+
function generateCodeVerifier() {
|
|
113
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
114
|
+
}
|
|
115
|
+
function generateCodeChallenge(verifier) {
|
|
116
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
117
|
+
}
|
|
118
|
+
function openBrowser(url) {
|
|
119
|
+
const platform = process.platform;
|
|
120
|
+
try {
|
|
121
|
+
if (platform === "darwin") {
|
|
122
|
+
launchDetached("open", [url]);
|
|
123
|
+
}
|
|
124
|
+
else if (platform === "win32") {
|
|
125
|
+
launchDetached("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
launchDetached("xdg-open", [url]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Best effort only. We always print the URL for manual fallback.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function extractData(value) {
|
|
136
|
+
if (value && typeof value === "object" && "data" in value) {
|
|
137
|
+
return value.data;
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function buildMissingAuthMessage() {
|
|
142
|
+
return [
|
|
143
|
+
"No Prior auth configured.",
|
|
144
|
+
"Run `prior-mcp --login` for browser OIDC auth,",
|
|
145
|
+
"configure PRIOR_API_KEY for durable machine auth,",
|
|
146
|
+
"or use PRIOR_ACCESS_TOKEN only for advanced manual token overrides.",
|
|
147
|
+
].join(" ");
|
|
148
|
+
}
|
|
149
|
+
function hardenConfigPermissions(filePath) {
|
|
150
|
+
const configDir = path.dirname(filePath);
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
try {
|
|
153
|
+
childProcess.execSync(`icacls "${configDir}" /inheritance:r /grant:r "%USERNAME%:(OI)(CI)F"`, {
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
shell: "cmd.exe",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Best effort only.
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
childProcess.execSync(`icacls "${filePath}" /inheritance:r /grant:r "%USERNAME%:F"`, {
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
shell: "cmd.exe",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Best effort only.
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
fs.chmodSync(configDir, 0o700);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Best effort only.
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
fs.chmodSync(filePath, 0o600);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Best effort only.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
48
185
|
class PriorApiClient {
|
|
49
186
|
apiUrl;
|
|
187
|
+
_authType;
|
|
50
188
|
_apiKey;
|
|
51
189
|
_agentId;
|
|
190
|
+
_accessToken;
|
|
191
|
+
_refreshToken;
|
|
192
|
+
_expiresAt;
|
|
193
|
+
_accountId;
|
|
194
|
+
_displayName;
|
|
195
|
+
_email;
|
|
52
196
|
persistConfig;
|
|
53
197
|
userAgent;
|
|
54
198
|
traceId;
|
|
@@ -56,26 +200,36 @@ class PriorApiClient {
|
|
|
56
200
|
this.apiUrl = options.apiUrl || process.env.PRIOR_API_URL || "https://api.cg3.io";
|
|
57
201
|
this._apiKey = options.apiKey || process.env.PRIOR_API_KEY;
|
|
58
202
|
this._agentId = options.agentId;
|
|
203
|
+
this._accessToken = options.accessToken || process.env.PRIOR_ACCESS_TOKEN;
|
|
204
|
+
this._refreshToken = options.refreshToken || process.env.PRIOR_REFRESH_TOKEN;
|
|
205
|
+
this._expiresAt = options.expiresAt;
|
|
59
206
|
this.persistConfig = options.persistConfig ?? true;
|
|
60
207
|
this.userAgent = options.userAgent || `prior-mcp/${VERSION}`;
|
|
61
208
|
this.traceId = options.traceId;
|
|
62
|
-
|
|
63
|
-
|
|
209
|
+
if (this._accessToken) {
|
|
210
|
+
this._authType = "oidc";
|
|
211
|
+
}
|
|
212
|
+
else if (this._apiKey) {
|
|
213
|
+
this._authType = "api_key";
|
|
214
|
+
}
|
|
215
|
+
if ((!this._apiKey && !this._accessToken) && this.persistConfig) {
|
|
64
216
|
const config = this.loadConfig();
|
|
65
217
|
if (config) {
|
|
66
|
-
this.
|
|
67
|
-
this._agentId = config.agentId;
|
|
218
|
+
this.applyConfig(config);
|
|
68
219
|
}
|
|
69
220
|
}
|
|
70
|
-
// Require an API key — no more auto-registration
|
|
71
|
-
if (!this._apiKey) {
|
|
72
|
-
throw new Error("No Prior API key configured. " +
|
|
73
|
-
"Get your key at https://prior.cg3.io/getkey then set PRIOR_API_KEY in your environment, " +
|
|
74
|
-
"or add it to ~/.prior/config.json as {\"apiKey\": \"...\"}.");
|
|
75
|
-
}
|
|
76
221
|
}
|
|
222
|
+
get authType() { return this._authType; }
|
|
77
223
|
get apiKey() { return this._apiKey; }
|
|
78
224
|
get agentId() { return this._agentId; }
|
|
225
|
+
get accessToken() { return this._accessToken; }
|
|
226
|
+
prefersOidc() {
|
|
227
|
+
if (this._authType === "oidc")
|
|
228
|
+
return true;
|
|
229
|
+
if (this._authType === "api_key")
|
|
230
|
+
return false;
|
|
231
|
+
return Boolean(this._accessToken || this._refreshToken);
|
|
232
|
+
}
|
|
79
233
|
loadConfig() {
|
|
80
234
|
try {
|
|
81
235
|
const raw = fs.readFileSync(exports.CONFIG_PATH, "utf-8");
|
|
@@ -86,15 +240,351 @@ class PriorApiClient {
|
|
|
86
240
|
}
|
|
87
241
|
}
|
|
88
242
|
saveConfig(config) {
|
|
89
|
-
fs.mkdirSync(path.dirname(exports.CONFIG_PATH), { recursive: true });
|
|
90
|
-
fs.writeFileSync(exports.CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
243
|
+
fs.mkdirSync(path.dirname(exports.CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
244
|
+
fs.writeFileSync(exports.CONFIG_PATH, JSON.stringify(compactConfig(config), null, 2));
|
|
245
|
+
hardenConfigPermissions(exports.CONFIG_PATH);
|
|
246
|
+
}
|
|
247
|
+
clearOidcConfig() {
|
|
248
|
+
const existing = this.loadConfig() || {};
|
|
249
|
+
const next = { ...existing };
|
|
250
|
+
delete next.authType;
|
|
251
|
+
delete next.accessToken;
|
|
252
|
+
delete next.refreshToken;
|
|
253
|
+
delete next.expiresAt;
|
|
254
|
+
delete next.accountId;
|
|
255
|
+
delete next.displayName;
|
|
256
|
+
delete next.email;
|
|
257
|
+
this.saveConfig(next);
|
|
258
|
+
this._accessToken = undefined;
|
|
259
|
+
this._refreshToken = undefined;
|
|
260
|
+
this._expiresAt = undefined;
|
|
261
|
+
this._accountId = undefined;
|
|
262
|
+
this._displayName = undefined;
|
|
263
|
+
this._email = undefined;
|
|
264
|
+
this._authType = this._apiKey ? "api_key" : undefined;
|
|
265
|
+
}
|
|
266
|
+
async logout() {
|
|
267
|
+
let remoteRevoked = false;
|
|
268
|
+
if (this._refreshToken) {
|
|
269
|
+
try {
|
|
270
|
+
const revokeUrl = new URL("/revoke", this.apiUrl).toString();
|
|
271
|
+
const response = await fetch(revokeUrl, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
275
|
+
"User-Agent": this.userAgent,
|
|
276
|
+
},
|
|
277
|
+
body: new URLSearchParams({
|
|
278
|
+
token: this._refreshToken,
|
|
279
|
+
token_type_hint: "refresh_token",
|
|
280
|
+
}).toString(),
|
|
281
|
+
signal: AbortSignal.timeout(5_000),
|
|
282
|
+
});
|
|
283
|
+
remoteRevoked = response.ok;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Best effort only. We always clear local state.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.clearOidcConfig();
|
|
290
|
+
return { remoteRevoked };
|
|
291
|
+
}
|
|
292
|
+
applyConfig(config) {
|
|
293
|
+
this._authType = config.authType
|
|
294
|
+
|| (config.accessToken ? "oidc" : config.apiKey ? "api_key" : undefined);
|
|
295
|
+
this._apiKey = this._apiKey || config.apiKey;
|
|
296
|
+
this._agentId = this._agentId || config.agentId;
|
|
297
|
+
this._accessToken = this._accessToken || config.accessToken;
|
|
298
|
+
this._refreshToken = this._refreshToken || config.refreshToken;
|
|
299
|
+
this._expiresAt = this._expiresAt || config.expiresAt;
|
|
300
|
+
this._accountId = this._accountId || config.accountId;
|
|
301
|
+
this._displayName = this._displayName || config.displayName;
|
|
302
|
+
this._email = this._email || config.email;
|
|
303
|
+
}
|
|
304
|
+
persistCurrentConfig(patch) {
|
|
305
|
+
if (!this.persistConfig)
|
|
306
|
+
return;
|
|
307
|
+
const base = this.loadConfig() || {};
|
|
308
|
+
this.saveConfig({ ...base, ...patch });
|
|
309
|
+
}
|
|
310
|
+
async loadDiscovery() {
|
|
311
|
+
const discoveryUrl = new URL("/.well-known/openid-configuration", this.apiUrl).toString();
|
|
312
|
+
const res = await fetch(discoveryUrl, {
|
|
313
|
+
headers: { "User-Agent": this.userAgent },
|
|
314
|
+
signal: AbortSignal.timeout(5_000),
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
throw new Error(`OIDC discovery failed (${res.status})`);
|
|
318
|
+
}
|
|
319
|
+
return await res.json();
|
|
320
|
+
}
|
|
321
|
+
async fetchUserInfo(token, explicitUrl) {
|
|
322
|
+
const userinfoUrl = explicitUrl || new URL("/userinfo", this.apiUrl).toString();
|
|
323
|
+
const res = await fetch(userinfoUrl, {
|
|
324
|
+
headers: {
|
|
325
|
+
"Authorization": `Bearer ${token}`,
|
|
326
|
+
"User-Agent": this.userAgent,
|
|
327
|
+
},
|
|
328
|
+
signal: AbortSignal.timeout(5_000),
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
throw new Error(`userinfo request failed (${res.status})`);
|
|
332
|
+
}
|
|
333
|
+
return await res.json();
|
|
334
|
+
}
|
|
335
|
+
async loginInteractive() {
|
|
336
|
+
const discovery = await this.loadDiscovery();
|
|
337
|
+
const authorizeUrl = discovery.authorization_endpoint || new URL("/authorize", this.apiUrl).toString();
|
|
338
|
+
const tokenUrl = discovery.token_endpoint || new URL("/token", this.apiUrl).toString();
|
|
339
|
+
const userinfoUrl = discovery.userinfo_endpoint || new URL("/userinfo", this.apiUrl).toString();
|
|
340
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
341
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
342
|
+
const codeVerifier = generateCodeVerifier();
|
|
343
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
344
|
+
return await new Promise((resolve, reject) => {
|
|
345
|
+
let settled = false;
|
|
346
|
+
const finishResolve = (config) => {
|
|
347
|
+
if (settled)
|
|
348
|
+
return;
|
|
349
|
+
settled = true;
|
|
350
|
+
clearTimeout(timeoutHandle);
|
|
351
|
+
resolve(config);
|
|
352
|
+
};
|
|
353
|
+
const finishReject = (error) => {
|
|
354
|
+
if (settled)
|
|
355
|
+
return;
|
|
356
|
+
settled = true;
|
|
357
|
+
clearTimeout(timeoutHandle);
|
|
358
|
+
reject(error);
|
|
359
|
+
};
|
|
360
|
+
const closeServer = () => {
|
|
361
|
+
try {
|
|
362
|
+
server.close();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Ignore close races when the callback and timeout fire near-simultaneously.
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const server = http.createServer(async (req, res) => {
|
|
369
|
+
if (!req.url?.startsWith("/callback")) {
|
|
370
|
+
res.writeHead(404);
|
|
371
|
+
res.end();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const callbackUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
375
|
+
const returnedState = callbackUrl.searchParams.get("state");
|
|
376
|
+
const code = callbackUrl.searchParams.get("code");
|
|
377
|
+
const error = callbackUrl.searchParams.get("error");
|
|
378
|
+
if (returnedState !== state) {
|
|
379
|
+
res.writeHead(403, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
380
|
+
res.end("<html><body><h1>Invalid state</h1><p>This login response did not match the original request.</p></body></html>");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (error) {
|
|
384
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
385
|
+
res.end(`<html><body><h1>Login cancelled</h1><p>${escapeHtml(error)}</p></body></html>`);
|
|
386
|
+
finishReject(new Error(`OIDC login cancelled: ${error}`));
|
|
387
|
+
closeServer();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!code) {
|
|
391
|
+
res.writeHead(400, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
392
|
+
res.end("<html><body><h1>Missing authorization code</h1></body></html>");
|
|
393
|
+
finishReject(new Error("OIDC login failed: missing authorization code"));
|
|
394
|
+
closeServer();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
399
|
+
const tokenRes = await fetch(tokenUrl, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: {
|
|
402
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
403
|
+
"User-Agent": this.userAgent,
|
|
404
|
+
},
|
|
405
|
+
body: new URLSearchParams({
|
|
406
|
+
grant_type: "authorization_code",
|
|
407
|
+
client_id: exports.OIDC_CLIENT_ID,
|
|
408
|
+
code,
|
|
409
|
+
code_verifier: codeVerifier,
|
|
410
|
+
redirect_uri: redirectUri,
|
|
411
|
+
}).toString(),
|
|
412
|
+
signal: AbortSignal.timeout(10_000),
|
|
413
|
+
});
|
|
414
|
+
const tokenBody = await tokenRes.json();
|
|
415
|
+
if (!tokenRes.ok || !tokenBody.access_token) {
|
|
416
|
+
const message = tokenBody.error_description || tokenBody.error || `OIDC token exchange failed (${tokenRes.status})`;
|
|
417
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
418
|
+
res.end(`<html><body><h1>Login failed</h1><p>${escapeHtml(message)}</p></body></html>`);
|
|
419
|
+
finishReject(new Error(message));
|
|
420
|
+
closeServer();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const userinfo = await this.fetchUserInfo(tokenBody.access_token, userinfoUrl);
|
|
424
|
+
const config = {
|
|
425
|
+
...(this.loadConfig() || {}),
|
|
426
|
+
authType: "oidc",
|
|
427
|
+
accessToken: tokenBody.access_token,
|
|
428
|
+
refreshToken: tokenBody.refresh_token,
|
|
429
|
+
expiresAt: deriveExpiresAt(tokenBody.access_token, tokenBody.expires_in),
|
|
430
|
+
accountId: userinfo.sub,
|
|
431
|
+
displayName: userinfo.name,
|
|
432
|
+
email: userinfo.email,
|
|
433
|
+
};
|
|
434
|
+
this.applyConfig(config);
|
|
435
|
+
this.persistCurrentConfig(config);
|
|
436
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
437
|
+
res.end(`<html><body><h1>Connected to Prior</h1><p>You can close this tab.</p><script>setTimeout(()=>window.close(), 1500)</script></body></html>`);
|
|
438
|
+
finishResolve(config);
|
|
439
|
+
closeServer();
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
443
|
+
res.writeHead(500, { "Content-Type": "text/html", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" });
|
|
444
|
+
res.end(`<html><body><h1>Login failed</h1><p>${escapeHtml(message)}</p></body></html>`);
|
|
445
|
+
finishReject(new Error(message));
|
|
446
|
+
closeServer();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
server.listen(0, "127.0.0.1", () => {
|
|
450
|
+
const address = server.address();
|
|
451
|
+
const redirectUri = `http://127.0.0.1:${address.port}/callback`;
|
|
452
|
+
const loginUrl = `${authorizeUrl}?${new URLSearchParams({
|
|
453
|
+
response_type: "code",
|
|
454
|
+
client_id: exports.OIDC_CLIENT_ID,
|
|
455
|
+
redirect_uri: redirectUri,
|
|
456
|
+
scope: DEFAULT_OIDC_SCOPES,
|
|
457
|
+
state,
|
|
458
|
+
nonce,
|
|
459
|
+
code_challenge: codeChallenge,
|
|
460
|
+
code_challenge_method: "S256",
|
|
461
|
+
}).toString()}`;
|
|
462
|
+
process.stderr.write(`[prior-mcp] Opening browser login for ${exports.OIDC_CLIENT_ID}\n`);
|
|
463
|
+
process.stderr.write(`[prior-mcp] If the browser does not open, visit: ${loginUrl}\n`);
|
|
464
|
+
openBrowser(loginUrl);
|
|
465
|
+
});
|
|
466
|
+
const timeoutHandle = setTimeout(() => {
|
|
467
|
+
closeServer();
|
|
468
|
+
finishReject(new Error("OIDC login timed out"));
|
|
469
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
470
|
+
timeoutHandle.unref?.();
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async refreshOidcAccessToken() {
|
|
474
|
+
if (!this._refreshToken) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const tokenUrl = new URL("/token", this.apiUrl).toString();
|
|
478
|
+
try {
|
|
479
|
+
const res = await fetch(tokenUrl, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: {
|
|
482
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
483
|
+
"User-Agent": this.userAgent,
|
|
484
|
+
},
|
|
485
|
+
body: new URLSearchParams({
|
|
486
|
+
grant_type: "refresh_token",
|
|
487
|
+
client_id: exports.OIDC_CLIENT_ID,
|
|
488
|
+
refresh_token: this._refreshToken,
|
|
489
|
+
}).toString(),
|
|
490
|
+
signal: AbortSignal.timeout(10_000),
|
|
491
|
+
});
|
|
492
|
+
const body = await res.json();
|
|
493
|
+
if (!res.ok || !body.access_token) {
|
|
494
|
+
this.clearOidcConfig();
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
this._authType = "oidc";
|
|
498
|
+
this._accessToken = body.access_token;
|
|
499
|
+
this._refreshToken = body.refresh_token || this._refreshToken;
|
|
500
|
+
this._expiresAt = deriveExpiresAt(body.access_token, body.expires_in);
|
|
501
|
+
this.persistCurrentConfig({
|
|
502
|
+
authType: "oidc",
|
|
503
|
+
accessToken: this._accessToken,
|
|
504
|
+
refreshToken: this._refreshToken,
|
|
505
|
+
expiresAt: this._expiresAt,
|
|
506
|
+
accountId: this._accountId,
|
|
507
|
+
displayName: this._displayName,
|
|
508
|
+
email: this._email,
|
|
509
|
+
});
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async ensureAuth() {
|
|
517
|
+
if (this.prefersOidc()) {
|
|
518
|
+
this._authType = "oidc";
|
|
519
|
+
if (this._accessToken && !isExpired(this._expiresAt, this._accessToken)) {
|
|
520
|
+
return this._accessToken;
|
|
521
|
+
}
|
|
522
|
+
const refreshed = await this.refreshOidcAccessToken();
|
|
523
|
+
if (refreshed && this._accessToken) {
|
|
524
|
+
return this._accessToken;
|
|
525
|
+
}
|
|
526
|
+
if (this._accessToken || this._refreshToken) {
|
|
527
|
+
throw new Error("Stored Prior OIDC auth is no longer usable. Run `prior-mcp --login` to refresh it " +
|
|
528
|
+
"or `prior-mcp --logout` to clear it before falling back to machine auth.");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (this._apiKey) {
|
|
532
|
+
this._authType = "api_key";
|
|
533
|
+
return this._apiKey;
|
|
534
|
+
}
|
|
535
|
+
throw new Error(buildMissingAuthMessage());
|
|
536
|
+
}
|
|
537
|
+
async getStatus() {
|
|
538
|
+
const auth = await this.ensureAuth();
|
|
539
|
+
if (this._authType === "oidc") {
|
|
540
|
+
const [accountEnvelope, profileEnvelope, userinfo] = await Promise.all([
|
|
541
|
+
this.request("GET", "/v1/account", undefined, auth),
|
|
542
|
+
this.request("GET", "/v1/prior/me/profile", undefined, auth),
|
|
543
|
+
this.fetchUserInfo(auth),
|
|
544
|
+
]);
|
|
545
|
+
const account = extractData(accountEnvelope);
|
|
546
|
+
const profile = extractData(profileEnvelope);
|
|
547
|
+
const displayName = userinfo.name || this._displayName;
|
|
548
|
+
const email = userinfo.email || this._email;
|
|
549
|
+
this._accountId = userinfo.sub || account?.account?.id || this._accountId;
|
|
550
|
+
this._displayName = displayName;
|
|
551
|
+
this._email = email;
|
|
552
|
+
this.persistCurrentConfig({
|
|
553
|
+
authType: "oidc",
|
|
554
|
+
accessToken: this._accessToken,
|
|
555
|
+
refreshToken: this._refreshToken,
|
|
556
|
+
expiresAt: this._expiresAt,
|
|
557
|
+
accountId: this._accountId,
|
|
558
|
+
displayName: this._displayName,
|
|
559
|
+
email: this._email,
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
id: account?.account?.id || userinfo.sub || "",
|
|
563
|
+
authType: "oidc",
|
|
564
|
+
credits: Number(profile?.subscription?.credits ?? 0),
|
|
565
|
+
tier: profile?.subscription?.tier || "free",
|
|
566
|
+
contributions: profile?.reputation?.contributionCount,
|
|
567
|
+
displayName,
|
|
568
|
+
email,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const data = await this.request("GET", "/v1/agents/me", undefined, auth);
|
|
572
|
+
const agent = extractData(data);
|
|
573
|
+
return {
|
|
574
|
+
id: agent?.id || "",
|
|
575
|
+
authType: "api_key",
|
|
576
|
+
credits: agent?.credits ?? 0,
|
|
577
|
+
tier: agent?.tier || "free",
|
|
578
|
+
contributions: agent?.contributions,
|
|
579
|
+
displayName: agent?.agentName,
|
|
580
|
+
};
|
|
91
581
|
}
|
|
92
|
-
async request(method,
|
|
93
|
-
const
|
|
94
|
-
const res = await fetch(`${this.apiUrl}${
|
|
582
|
+
async request(method, requestPath, body, key) {
|
|
583
|
+
const auth = key || await this.ensureAuth();
|
|
584
|
+
const res = await fetch(`${this.apiUrl}${requestPath}`, {
|
|
95
585
|
method,
|
|
96
586
|
headers: {
|
|
97
|
-
|
|
587
|
+
"Authorization": `Bearer ${auth}`,
|
|
98
588
|
"Content-Type": "application/json",
|
|
99
589
|
"User-Agent": this.userAgent,
|
|
100
590
|
...(this.traceId ? { "X-Request-Id": this.traceId } : {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { PriorApiClient, type PriorConfig } from "./client.js";
|
|
3
4
|
export { CONFIG_PATH, PriorApiClient } from "./client.js";
|
|
4
5
|
export { registerTools } from "./tools.js";
|
|
5
6
|
export { registerResources } from "./resources.js";
|
|
6
7
|
export { detectHost, formatResults } from "./utils.js";
|
|
7
|
-
export declare function loadConfig():
|
|
8
|
-
export declare function saveConfig(config:
|
|
9
|
-
|
|
10
|
-
agentId: string;
|
|
11
|
-
}): void;
|
|
12
|
-
export declare function createServer(): McpServer;
|
|
8
|
+
export declare function loadConfig(): PriorConfig | null;
|
|
9
|
+
export declare function saveConfig(config: PriorConfig): void;
|
|
10
|
+
export declare function createServer(client?: PriorApiClient): McpServer;
|
|
13
11
|
export declare function main(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -24,22 +24,42 @@ Object.defineProperty(exports, "registerResources", { enumerable: true, get: fun
|
|
|
24
24
|
var utils_js_1 = require("./utils.js");
|
|
25
25
|
Object.defineProperty(exports, "detectHost", { enumerable: true, get: function () { return utils_js_1.detectHost; } });
|
|
26
26
|
Object.defineProperty(exports, "formatResults", { enumerable: true, get: function () { return utils_js_1.formatResults; } });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
(0, tools_js_1.registerTools)(server, { client });
|
|
38
|
-
(0, resources_js_1.registerResources)(server, { client });
|
|
39
|
-
function createServer() {
|
|
27
|
+
function buildServer(client) {
|
|
28
|
+
const server = new mcp_js_1.McpServer({
|
|
29
|
+
name: "prior",
|
|
30
|
+
version: "0.6.4",
|
|
31
|
+
}, {
|
|
32
|
+
instructions: `Prior is a knowledge base of agent-discovered solutions to technical problems and research questions, including debugging, troubleshooting, configuration, integration, migration, architecture decisions, and more. Search Prior before investigating errors or starting multi-step research. Tool responses handle feedback and contribution prompts. See prior://docs/agent-guide for usage patterns.`,
|
|
33
|
+
});
|
|
34
|
+
(0, tools_js_1.registerTools)(server, { client });
|
|
35
|
+
(0, resources_js_1.registerResources)(server, { client });
|
|
40
36
|
return server;
|
|
41
37
|
}
|
|
38
|
+
// Legacy function exports for backward compatibility
|
|
39
|
+
function loadConfig() {
|
|
40
|
+
return new client_js_1.PriorApiClient({ persistConfig: true }).loadConfig();
|
|
41
|
+
}
|
|
42
|
+
function saveConfig(config) {
|
|
43
|
+
return new client_js_1.PriorApiClient({ persistConfig: true }).saveConfig(config);
|
|
44
|
+
}
|
|
45
|
+
function createServer(client = new client_js_1.PriorApiClient()) {
|
|
46
|
+
return buildServer(client);
|
|
47
|
+
}
|
|
42
48
|
async function main() {
|
|
49
|
+
if (process.argv.includes("--login")) {
|
|
50
|
+
const client = new client_js_1.PriorApiClient({ persistConfig: true });
|
|
51
|
+
const config = await client.loginInteractive();
|
|
52
|
+
const subject = config.displayName || config.email || config.accountId || "Prior user";
|
|
53
|
+
console.error(`[prior-mcp] Browser login complete for ${subject}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (process.argv.includes("--logout")) {
|
|
57
|
+
const client = new client_js_1.PriorApiClient({ persistConfig: true });
|
|
58
|
+
await client.logout();
|
|
59
|
+
console.error("[prior-mcp] Cleared stored OIDC login. API key config, if any, was preserved.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const server = createServer();
|
|
43
63
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
44
64
|
await server.connect(transport);
|
|
45
65
|
}
|
package/dist/resources.d.ts
CHANGED