@bettercms-ai/mcp 0.27.0 → 0.28.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/index.d.ts +3 -123
- package/dist/index.js +5 -305
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,126 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Resolved configuration for the BetterCMS MCP server.
|
|
5
|
-
*
|
|
6
|
-
* A single `BETTERCMS_API_URL` (origin, no path) drives both the device-auth
|
|
7
|
-
* endpoints and the Management API base the SDK targets:
|
|
8
|
-
* device: {apiUrl}/api/v1/auth/device/*
|
|
9
|
-
* management: {apiUrl}/api/v1 (SDK appends /management/content/*)
|
|
10
|
-
*/
|
|
11
|
-
interface McpConfig {
|
|
12
|
-
apiUrl: string;
|
|
13
|
-
deviceBaseUrl: string;
|
|
14
|
-
managementBaseUrl: string;
|
|
15
|
-
credentialsPath: string;
|
|
16
|
-
clientName: string;
|
|
17
|
-
}
|
|
18
|
-
declare function loadConfig(env?: NodeJS.ProcessEnv): McpConfig;
|
|
19
|
-
|
|
20
|
-
/** Credentials cached between runs so the device flow runs only once per env. */
|
|
21
|
-
interface StoredCredentials {
|
|
22
|
-
accessToken: string;
|
|
23
|
-
refreshToken: string;
|
|
24
|
-
/** Epoch ms when the access token expires. */
|
|
25
|
-
accessTokenExpiresAt: number;
|
|
26
|
-
workspaceId: string | null;
|
|
27
|
-
projectId: string | null;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* An authorization the user has been sent off to approve but hasn't yet.
|
|
31
|
-
* Persisted so a later tool call can *resume* polling that same code instead of
|
|
32
|
-
* minting a fresh one — this is what lets the flow survive across the
|
|
33
|
-
* "return the link → user approves → retry" round-trip in clients (VS Code)
|
|
34
|
-
* that never surface the server's stderr prompt.
|
|
35
|
-
*/
|
|
36
|
-
interface PendingDevice {
|
|
37
|
-
deviceCode: string;
|
|
38
|
-
userCode: string;
|
|
39
|
-
verificationUri: string;
|
|
40
|
-
/** verification_uri with `?code=` prefilled — the link we hand the user. */
|
|
41
|
-
verificationUriComplete: string;
|
|
42
|
-
intervalSeconds: number;
|
|
43
|
-
/** Epoch ms when the device code expires. */
|
|
44
|
-
expiresAt: number;
|
|
45
|
-
}
|
|
46
|
-
/** Persistence boundary for credentials (file-backed in prod, in-memory in tests). */
|
|
47
|
-
interface TokenStore {
|
|
48
|
-
read(): Promise<StoredCredentials | null>;
|
|
49
|
-
write(creds: StoredCredentials): Promise<void>;
|
|
50
|
-
clear(): Promise<void>;
|
|
51
|
-
/** In-progress device authorization awaiting approval, if any. */
|
|
52
|
-
readPending(): Promise<PendingDevice | null>;
|
|
53
|
-
writePending(pending: PendingDevice): Promise<void>;
|
|
54
|
-
clearPending(): Promise<void>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Injectable seams so tests can run without real timers / network / stderr. */
|
|
58
|
-
interface DeviceAuthDeps {
|
|
59
|
-
fetch?: typeof fetch;
|
|
60
|
-
sleep?: (ms: number) => Promise<void>;
|
|
61
|
-
log?: (message: string) => void;
|
|
62
|
-
now?: () => number;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Drives the OAuth 2.0 Device Authorization Grant (RFC 8628) against the
|
|
66
|
-
* BetterCMS backend and hands the SDK a valid `content:manage` access token.
|
|
67
|
-
*
|
|
68
|
-
* - `getAccessToken()` returns a usable token: cached if fresh, refreshed if
|
|
69
|
-
* expired, or freshly minted via the full device flow if there's nothing valid.
|
|
70
|
-
* - All human-facing output goes to stderr — stdout is the MCP JSON-RPC channel.
|
|
71
|
-
*/
|
|
72
|
-
declare class DeviceAuthClient {
|
|
73
|
-
private readonly config;
|
|
74
|
-
private readonly store;
|
|
75
|
-
private readonly fetchImpl;
|
|
76
|
-
private readonly sleep;
|
|
77
|
-
private readonly log;
|
|
78
|
-
private readonly now;
|
|
79
|
-
private inFlight;
|
|
80
|
-
private refreshInFlight;
|
|
81
|
-
/** The single live poller for the current device code (see runDeviceFlow). */
|
|
82
|
-
private pollTask;
|
|
83
|
-
constructor(config: McpConfig, store: TokenStore, deps?: DeviceAuthDeps);
|
|
84
|
-
/** Return a valid access token, doing the least work necessary. Single-flighted. */
|
|
85
|
-
getAccessToken(): Promise<string>;
|
|
86
|
-
private resolveToken;
|
|
87
|
-
/**
|
|
88
|
-
* Resume a still-live authorization if one is persisted, otherwise start a
|
|
89
|
-
* fresh one; then grace-poll. Throws {@link DeviceAuthPendingError} (carrying
|
|
90
|
-
* the activation link) if the user hasn't approved within the grace window.
|
|
91
|
-
*/
|
|
92
|
-
private runDeviceFlow;
|
|
93
|
-
/** Keep redeeming this code until it expires, detached from any tool call. One per code. */
|
|
94
|
-
private pollInBackground;
|
|
95
|
-
/** Request a fresh device code, persist it as pending, and log a breadcrumb. */
|
|
96
|
-
private startDeviceFlow;
|
|
97
|
-
/**
|
|
98
|
-
* Poll the token endpoint until `deadline`. Returns the access token on
|
|
99
|
-
* approval, or null if the deadline passes while still pending. Throws
|
|
100
|
-
* {@link DeviceAuthError} on a terminal outcome (denied / expired).
|
|
101
|
-
*/
|
|
102
|
-
private pollForApproval;
|
|
103
|
-
/**
|
|
104
|
-
* Exchange the stored refresh token for a new access token. Single-flighted:
|
|
105
|
-
* the device `/refresh` endpoint is single-use (it rotates the refresh token
|
|
106
|
-
* and revokes the prior access key), so a burst of concurrent 401s must NOT
|
|
107
|
-
* each fire their own refresh — the first would rotate, and the rest would
|
|
108
|
-
* send the now-stale token, get `invalid_grant`, and wipe the freshly-minted
|
|
109
|
-
* credentials. Collapsing them into one in-flight rotation keeps the session
|
|
110
|
-
* alive without a needless re-auth.
|
|
111
|
-
*/
|
|
112
|
-
refresh(): Promise<string | null>;
|
|
113
|
-
/**
|
|
114
|
-
* Forget the cached credentials and start a fresh device flow. Called when the
|
|
115
|
-
* bound project was deleted server-side (a key bound to a dead project can never
|
|
116
|
-
* succeed again) — clearing lets the user re-authorize against a LIVE project.
|
|
117
|
-
* Returns a new token if approval is fast, else throws {@link DeviceAuthPendingError}
|
|
118
|
-
* carrying the activation link (the next tool call resumes into the new project).
|
|
119
|
-
*/
|
|
120
|
-
resetAndReauthorize(): Promise<string>;
|
|
121
|
-
private doRefresh;
|
|
122
|
-
private persist;
|
|
123
|
-
}
|
|
2
|
+
import { DeviceAuthClient } from '@bettercms-ai/device-auth';
|
|
3
|
+
export { DeviceAuthClient, loadConfig } from '@bettercms-ai/device-auth';
|
|
124
4
|
|
|
125
5
|
interface BuildServerDeps {
|
|
126
6
|
auth: DeviceAuthClient;
|
|
@@ -133,4 +13,4 @@ interface BuildServerDeps {
|
|
|
133
13
|
*/
|
|
134
14
|
declare function buildServer(deps: BuildServerDeps): McpServer;
|
|
135
15
|
|
|
136
|
-
export {
|
|
16
|
+
export { buildServer };
|
package/dist/index.js
CHANGED
|
@@ -4,309 +4,7 @@
|
|
|
4
4
|
import { realpathSync } from "fs";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
|
|
8
|
-
// src/config.ts
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
var DEFAULT_API_URL = "https://api.bettercms.ai";
|
|
12
|
-
function loadConfig(env = process.env) {
|
|
13
|
-
const apiUrl = (env.BETTERCMS_API_URL?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
14
|
-
return {
|
|
15
|
-
apiUrl,
|
|
16
|
-
deviceBaseUrl: `${apiUrl}/api/v1/auth/device`,
|
|
17
|
-
managementBaseUrl: `${apiUrl}/api/v1`,
|
|
18
|
-
credentialsPath: env.BETTERCMS_MCP_CREDENTIALS?.trim() || join(homedir(), ".bettercms", "mcp-credentials.json"),
|
|
19
|
-
clientName: env.BETTERCMS_MCP_CLIENT_NAME?.trim() || "BetterCMS MCP"
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// src/token-store.ts
|
|
24
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
25
|
-
import { dirname } from "path";
|
|
26
|
-
var FileTokenStore = class {
|
|
27
|
-
constructor(path, key) {
|
|
28
|
-
this.path = path;
|
|
29
|
-
this.key = key;
|
|
30
|
-
this.pendingKey = `${key}::pending`;
|
|
31
|
-
}
|
|
32
|
-
path;
|
|
33
|
-
key;
|
|
34
|
-
/** Pending authorizations live under a sibling key so they never shadow creds. */
|
|
35
|
-
pendingKey;
|
|
36
|
-
async readAll() {
|
|
37
|
-
try {
|
|
38
|
-
const raw = await readFile(this.path, "utf-8");
|
|
39
|
-
const parsed = JSON.parse(raw);
|
|
40
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
41
|
-
} catch {
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async writeAll(all) {
|
|
46
|
-
await mkdir(dirname(this.path), { recursive: true });
|
|
47
|
-
await writeFile(this.path, JSON.stringify(all, null, 2), { mode: 384 });
|
|
48
|
-
}
|
|
49
|
-
async read() {
|
|
50
|
-
const all = await this.readAll();
|
|
51
|
-
return all[this.key] ?? null;
|
|
52
|
-
}
|
|
53
|
-
async write(creds) {
|
|
54
|
-
const all = await this.readAll();
|
|
55
|
-
all[this.key] = creds;
|
|
56
|
-
await this.writeAll(all);
|
|
57
|
-
}
|
|
58
|
-
async clear() {
|
|
59
|
-
const all = await this.readAll();
|
|
60
|
-
delete all[this.key];
|
|
61
|
-
await this.writeAll(all);
|
|
62
|
-
}
|
|
63
|
-
async readPending() {
|
|
64
|
-
const all = await this.readAll();
|
|
65
|
-
return all[this.pendingKey] ?? null;
|
|
66
|
-
}
|
|
67
|
-
async writePending(pending) {
|
|
68
|
-
const all = await this.readAll();
|
|
69
|
-
all[this.pendingKey] = pending;
|
|
70
|
-
await this.writeAll(all);
|
|
71
|
-
}
|
|
72
|
-
async clearPending() {
|
|
73
|
-
const all = await this.readAll();
|
|
74
|
-
delete all[this.pendingKey];
|
|
75
|
-
await this.writeAll(all);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// src/device-auth.ts
|
|
80
|
-
var EXPIRY_SKEW_MS = 6e4;
|
|
81
|
-
var GRACE_POLL_MS = 25e3;
|
|
82
|
-
var DeviceAuthError = class extends Error {
|
|
83
|
-
constructor(message) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.name = "DeviceAuthError";
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
var DeviceAuthPendingError = class extends Error {
|
|
89
|
-
verificationUri;
|
|
90
|
-
verificationUriComplete;
|
|
91
|
-
userCode;
|
|
92
|
-
expiresAt;
|
|
93
|
-
constructor(pending) {
|
|
94
|
-
super("Authorization pending \u2014 approve in the browser, then retry.");
|
|
95
|
-
this.name = "DeviceAuthPendingError";
|
|
96
|
-
this.verificationUri = pending.verificationUri;
|
|
97
|
-
this.verificationUriComplete = pending.verificationUriComplete;
|
|
98
|
-
this.userCode = pending.userCode;
|
|
99
|
-
this.expiresAt = pending.expiresAt;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
var DeviceAuthClient = class {
|
|
103
|
-
constructor(config, store, deps = {}) {
|
|
104
|
-
this.config = config;
|
|
105
|
-
this.store = store;
|
|
106
|
-
this.fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
107
|
-
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
108
|
-
this.log = deps.log ?? ((m) => process.stderr.write(`${m}
|
|
109
|
-
`));
|
|
110
|
-
this.now = deps.now ?? (() => Date.now());
|
|
111
|
-
}
|
|
112
|
-
config;
|
|
113
|
-
store;
|
|
114
|
-
fetchImpl;
|
|
115
|
-
sleep;
|
|
116
|
-
log;
|
|
117
|
-
now;
|
|
118
|
-
inFlight = null;
|
|
119
|
-
refreshInFlight = null;
|
|
120
|
-
/** The single live poller for the current device code (see runDeviceFlow). */
|
|
121
|
-
pollTask = null;
|
|
122
|
-
/** Return a valid access token, doing the least work necessary. Single-flighted. */
|
|
123
|
-
async getAccessToken() {
|
|
124
|
-
if (this.inFlight) return this.inFlight;
|
|
125
|
-
this.inFlight = this.resolveToken().finally(() => {
|
|
126
|
-
this.inFlight = null;
|
|
127
|
-
});
|
|
128
|
-
return this.inFlight;
|
|
129
|
-
}
|
|
130
|
-
async resolveToken() {
|
|
131
|
-
const creds = await this.store.read();
|
|
132
|
-
if (creds && creds.accessTokenExpiresAt - this.now() > EXPIRY_SKEW_MS) {
|
|
133
|
-
return creds.accessToken;
|
|
134
|
-
}
|
|
135
|
-
if (creds?.refreshToken) {
|
|
136
|
-
const refreshed = await this.refresh();
|
|
137
|
-
if (refreshed) return refreshed;
|
|
138
|
-
}
|
|
139
|
-
return this.runDeviceFlow();
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Resume a still-live authorization if one is persisted, otherwise start a
|
|
143
|
-
* fresh one; then grace-poll. Throws {@link DeviceAuthPendingError} (carrying
|
|
144
|
-
* the activation link) if the user hasn't approved within the grace window.
|
|
145
|
-
*/
|
|
146
|
-
async runDeviceFlow() {
|
|
147
|
-
let pending = await this.store.readPending();
|
|
148
|
-
if (pending && pending.expiresAt - this.now() <= EXPIRY_SKEW_MS) {
|
|
149
|
-
await this.store.clearPending();
|
|
150
|
-
pending = null;
|
|
151
|
-
}
|
|
152
|
-
if (!pending) {
|
|
153
|
-
pending = await this.startDeviceFlow();
|
|
154
|
-
}
|
|
155
|
-
const graceDeadline = Math.min(this.now() + GRACE_POLL_MS, pending.expiresAt);
|
|
156
|
-
const token = this.pollTask ? await Promise.race([this.pollTask, this.sleep(GRACE_POLL_MS).then(() => null)]) : await this.pollForApproval(pending, graceDeadline);
|
|
157
|
-
if (token) return token;
|
|
158
|
-
this.pollInBackground(pending);
|
|
159
|
-
throw new DeviceAuthPendingError(pending);
|
|
160
|
-
}
|
|
161
|
-
/** Keep redeeming this code until it expires, detached from any tool call. One per code. */
|
|
162
|
-
pollInBackground(pending) {
|
|
163
|
-
if (this.pollTask) return;
|
|
164
|
-
const task = this.pollForApproval(pending, pending.expiresAt);
|
|
165
|
-
this.pollTask = task;
|
|
166
|
-
void task.catch(() => {
|
|
167
|
-
}).finally(() => {
|
|
168
|
-
if (this.pollTask === task) this.pollTask = null;
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
/** Request a fresh device code, persist it as pending, and log a breadcrumb. */
|
|
172
|
-
async startDeviceFlow() {
|
|
173
|
-
const start = await this.fetchImpl(`${this.config.deviceBaseUrl}/code`, {
|
|
174
|
-
method: "POST",
|
|
175
|
-
headers: { "Content-Type": "application/json" },
|
|
176
|
-
body: JSON.stringify({ client_name: this.config.clientName })
|
|
177
|
-
});
|
|
178
|
-
if (!start.ok) {
|
|
179
|
-
throw new DeviceAuthError(
|
|
180
|
-
`Failed to start device authorization (HTTP ${start.status}).`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
const code = await start.json();
|
|
184
|
-
const pending = {
|
|
185
|
-
deviceCode: code.device_code,
|
|
186
|
-
userCode: code.user_code,
|
|
187
|
-
verificationUri: code.verification_uri,
|
|
188
|
-
verificationUriComplete: code.verification_uri_complete ?? `${code.verification_uri}?code=${encodeURIComponent(code.user_code)}`,
|
|
189
|
-
intervalSeconds: code.interval,
|
|
190
|
-
expiresAt: this.now() + code.expires_in * 1e3
|
|
191
|
-
};
|
|
192
|
-
await this.store.writePending(pending);
|
|
193
|
-
this.pollTask = null;
|
|
194
|
-
this.log("");
|
|
195
|
-
this.log("\u250C\u2500 BetterCMS authorization required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
196
|
-
this.log(`\u2502 Visit: ${pending.verificationUri}`);
|
|
197
|
-
this.log(`\u2502 Enter code: ${pending.userCode}`);
|
|
198
|
-
this.log(`\u2502 Or open: ${pending.verificationUriComplete}`);
|
|
199
|
-
this.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
200
|
-
return pending;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Poll the token endpoint until `deadline`. Returns the access token on
|
|
204
|
-
* approval, or null if the deadline passes while still pending. Throws
|
|
205
|
-
* {@link DeviceAuthError} on a terminal outcome (denied / expired).
|
|
206
|
-
*/
|
|
207
|
-
async pollForApproval(pending, deadline) {
|
|
208
|
-
let intervalMs = pending.intervalSeconds * 1e3;
|
|
209
|
-
while (this.now() < deadline) {
|
|
210
|
-
await this.sleep(intervalMs);
|
|
211
|
-
if (this.now() >= deadline) break;
|
|
212
|
-
const res = await this.fetchImpl(`${this.config.deviceBaseUrl}/token`, {
|
|
213
|
-
method: "POST",
|
|
214
|
-
headers: { "Content-Type": "application/json" },
|
|
215
|
-
body: JSON.stringify({
|
|
216
|
-
device_code: pending.deviceCode,
|
|
217
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
218
|
-
})
|
|
219
|
-
});
|
|
220
|
-
if (res.ok) {
|
|
221
|
-
const body = await res.json();
|
|
222
|
-
await this.store.clearPending();
|
|
223
|
-
this.log("[bettercms-mcp] authorized \u2713");
|
|
224
|
-
return this.persist(body);
|
|
225
|
-
}
|
|
226
|
-
const err = await res.json().catch(() => ({}));
|
|
227
|
-
switch (err.error) {
|
|
228
|
-
case "authorization_pending":
|
|
229
|
-
continue;
|
|
230
|
-
case "slow_down":
|
|
231
|
-
intervalMs += 5e3;
|
|
232
|
-
continue;
|
|
233
|
-
case "access_denied":
|
|
234
|
-
await this.store.clearPending();
|
|
235
|
-
throw new DeviceAuthError("Authorization was denied.");
|
|
236
|
-
case "expired_token":
|
|
237
|
-
await this.store.clearPending();
|
|
238
|
-
throw new DeviceAuthError("The device code expired before approval. Try again.");
|
|
239
|
-
default:
|
|
240
|
-
throw new DeviceAuthError(
|
|
241
|
-
`Device authorization failed: ${err.error ?? `HTTP ${res.status}`}.`
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Exchange the stored refresh token for a new access token. Single-flighted:
|
|
249
|
-
* the device `/refresh` endpoint is single-use (it rotates the refresh token
|
|
250
|
-
* and revokes the prior access key), so a burst of concurrent 401s must NOT
|
|
251
|
-
* each fire their own refresh — the first would rotate, and the rest would
|
|
252
|
-
* send the now-stale token, get `invalid_grant`, and wipe the freshly-minted
|
|
253
|
-
* credentials. Collapsing them into one in-flight rotation keeps the session
|
|
254
|
-
* alive without a needless re-auth.
|
|
255
|
-
*/
|
|
256
|
-
async refresh() {
|
|
257
|
-
if (this.refreshInFlight) return this.refreshInFlight;
|
|
258
|
-
this.refreshInFlight = this.doRefresh().finally(() => {
|
|
259
|
-
this.refreshInFlight = null;
|
|
260
|
-
});
|
|
261
|
-
return this.refreshInFlight;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Forget the cached credentials and start a fresh device flow. Called when the
|
|
265
|
-
* bound project was deleted server-side (a key bound to a dead project can never
|
|
266
|
-
* succeed again) — clearing lets the user re-authorize against a LIVE project.
|
|
267
|
-
* Returns a new token if approval is fast, else throws {@link DeviceAuthPendingError}
|
|
268
|
-
* carrying the activation link (the next tool call resumes into the new project).
|
|
269
|
-
*/
|
|
270
|
-
async resetAndReauthorize() {
|
|
271
|
-
await this.store.clear();
|
|
272
|
-
await this.store.clearPending();
|
|
273
|
-
return this.getAccessToken();
|
|
274
|
-
}
|
|
275
|
-
async doRefresh() {
|
|
276
|
-
const creds = await this.store.read();
|
|
277
|
-
if (!creds?.refreshToken) return null;
|
|
278
|
-
let res;
|
|
279
|
-
try {
|
|
280
|
-
res = await this.fetchImpl(`${this.config.deviceBaseUrl}/refresh`, {
|
|
281
|
-
method: "POST",
|
|
282
|
-
headers: { "Content-Type": "application/json" },
|
|
283
|
-
body: JSON.stringify({ refresh_token: creds.refreshToken })
|
|
284
|
-
});
|
|
285
|
-
} catch {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
if (res.ok) {
|
|
289
|
-
const body = await res.json();
|
|
290
|
-
return this.persist(body);
|
|
291
|
-
}
|
|
292
|
-
const err = await res.json().catch(() => ({}));
|
|
293
|
-
if (err.error === "invalid_grant" || res.status === 401 || res.status === 403) {
|
|
294
|
-
await this.store.clear();
|
|
295
|
-
}
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
async persist(body) {
|
|
299
|
-
const creds = {
|
|
300
|
-
accessToken: body.access_token,
|
|
301
|
-
refreshToken: body.refresh_token,
|
|
302
|
-
accessTokenExpiresAt: this.now() + body.expires_in * 1e3,
|
|
303
|
-
workspaceId: body.workspace_id,
|
|
304
|
-
projectId: body.project_id
|
|
305
|
-
};
|
|
306
|
-
await this.store.write(creds);
|
|
307
|
-
return creds.accessToken;
|
|
308
|
-
}
|
|
309
|
-
};
|
|
7
|
+
import { loadConfig, FileTokenStore, DeviceAuthClient } from "@bettercms-ai/device-auth";
|
|
310
8
|
|
|
311
9
|
// src/server.ts
|
|
312
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -2282,6 +1980,7 @@ var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
|
|
|
2282
1980
|
|
|
2283
1981
|
// src/tools.ts
|
|
2284
1982
|
import { BetterCMSError } from "@bettercms-ai/sdk";
|
|
1983
|
+
import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
|
|
2285
1984
|
var FRAMEWORK_CHOICES = ["astro", "next", "react-ts", "other"];
|
|
2286
1985
|
var FRAMEWORK_LABELS = {
|
|
2287
1986
|
astro: "Astro \u2014 recommended default, static by default and fastest to publish",
|
|
@@ -4924,6 +4623,7 @@ function buildServer(deps) {
|
|
|
4924
4623
|
}
|
|
4925
4624
|
|
|
4926
4625
|
// src/index.ts
|
|
4626
|
+
import { DeviceAuthClient as DeviceAuthClient2, loadConfig as loadConfig2 } from "@bettercms-ai/device-auth";
|
|
4927
4627
|
async function main() {
|
|
4928
4628
|
const config = loadConfig();
|
|
4929
4629
|
const store = new FileTokenStore(config.credentialsPath, config.apiUrl);
|
|
@@ -4951,8 +4651,8 @@ if (isMainModule()) {
|
|
|
4951
4651
|
});
|
|
4952
4652
|
}
|
|
4953
4653
|
export {
|
|
4954
|
-
DeviceAuthClient,
|
|
4654
|
+
DeviceAuthClient2 as DeviceAuthClient,
|
|
4955
4655
|
buildServer,
|
|
4956
|
-
loadConfig
|
|
4656
|
+
loadConfig2 as loadConfig
|
|
4957
4657
|
};
|
|
4958
4658
|
//# sourceMappingURL=index.js.map
|