constructor-research-mcp 0.3.2 → 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +24 -25
- package/README.md +34 -50
- package/dist/api.d.ts +11 -30
- package/dist/api.js +53 -130
- package/dist/config.d.ts +8 -22
- package/dist/config.js +9 -45
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +26 -13
- package/dist/files.js +1 -1
- package/dist/key.d.ts +9 -0
- package/dist/key.js +35 -0
- package/dist/location.js +2 -1
- package/dist/logs.js +2 -2
- package/dist/runtime.js +4 -4
- package/dist/schemas.d.ts +0 -5
- package/dist/schemas.js +0 -18
- package/dist/server.js +9 -104
- package/dist/set-key.js +107 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.js +1 -1
- package/dist/wait.js +3 -6
- package/examples/claude-hooks.json +1 -1
- package/examples/claude.mcp.json +1 -1
- package/examples/codex-hooks.json +1 -1
- package/examples/codex.toml +1 -1
- package/examples/opencode-plugin.js +1 -1
- package/examples/opencode-v2.json +1 -1
- package/examples/opencode.json +1 -1
- package/package.json +7 -6
- package/dist/login.js +0 -25
- package/dist/session.d.ts +0 -21
- package/dist/session.js +0 -108
- package/dist/set-token.d.ts +0 -2
- package/dist/set-token.js +0 -48
- package/dist/signin.d.ts +0 -19
- package/dist/signin.js +0 -209
- /package/dist/{login.d.ts → set-key.d.ts} +0 -0
package/dist/set-token.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Save the session from a copied cURL (or a bare token) for the MCP without printing it.
|
|
3
|
-
// Usage: npx -y -p constructor-research-mcp constructor-research-set-token <file with a copied cURL, or a token> --for claude-code|codex|opencode
|
|
4
|
-
// (or --to <session file>); the session belongs to the Constructor the cURL was sent to.
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
import { configFromEnv, HARNESSES, originOf, sessionArgs } from "./config.js";
|
|
7
|
-
import { claims, saveSession, sessionCookies } from "./session.js";
|
|
8
|
-
const { file: target, rest } = sessionArgs(process.argv.slice(2), configFromEnv());
|
|
9
|
-
const source = rest[0];
|
|
10
|
-
if (!source || !target) {
|
|
11
|
-
console.error(`Usage: npx -y -p constructor-research-mcp constructor-research-set-token <file with a copied cURL> --for ${HARNESSES.join("|")} (or --to <session file>)`);
|
|
12
|
-
process.exit(2);
|
|
13
|
-
}
|
|
14
|
-
const text = readFileSync(source, "utf8");
|
|
15
|
-
const found = sessionCookies(text);
|
|
16
|
-
const token = found.get("ap_access_token");
|
|
17
|
-
if (!token) {
|
|
18
|
-
console.error("No ap_access_token found in " + source + ".");
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
const { exp, max_slt } = claims(token) ?? {};
|
|
22
|
-
if (exp && exp * 1000 <= Date.now() && !found.has("ap_refresh_token")) {
|
|
23
|
-
console.error("That token already expired at " +
|
|
24
|
-
new Date(exp * 1000).toISOString() +
|
|
25
|
-
"; copy a fresh one.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
// The Constructor the cURL was sent to; a bare token names its own issuer.
|
|
29
|
-
const sent = /\bcurl\s+(?:-\S+\s+)*['"]?(https?:\/\/[^\s'"]+)/.exec(text)?.[1];
|
|
30
|
-
let host;
|
|
31
|
-
try {
|
|
32
|
-
host = sent ? originOf(sent) : undefined;
|
|
33
|
-
}
|
|
34
|
-
catch { }
|
|
35
|
-
await saveSession(target, found, host);
|
|
36
|
-
const time = (s) => new Date(s * 1000).toISOString().slice(0, 16).replace("T", " ") + " UTC";
|
|
37
|
-
console.log("Saved to " +
|
|
38
|
-
target +
|
|
39
|
-
(host ? ` (for ${host})` : "") +
|
|
40
|
-
(found.has("ap_refresh_token")
|
|
41
|
-
? `, with the refresh token: the MCP renews the session itself${max_slt ? " until " + time(max_slt) : ""}. The browser this cURL came from holds the same session, and a refresh token works once: whichever renews first logs the other out. The sign_in tool (or constructor-research-login) gives the MCP a session of its own.`
|
|
42
|
-
: exp
|
|
43
|
-
? "; expires " +
|
|
44
|
-
time(exp) +
|
|
45
|
-
" (in " +
|
|
46
|
-
Math.round((exp * 1000 - Date.now()) / 60000) +
|
|
47
|
-
" min). Copy the cURL with the ap_refresh_token cookie, or use the sign_in tool, and the MCP renews it itself."
|
|
48
|
-
: "."));
|
package/dist/signin.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Config } from "./config.js";
|
|
2
|
-
export type SignInResult = {
|
|
3
|
-
status: "signed_in";
|
|
4
|
-
message: string;
|
|
5
|
-
session_file: string;
|
|
6
|
-
host: string;
|
|
7
|
-
until?: string;
|
|
8
|
-
} | {
|
|
9
|
-
status: "sign_in_failed";
|
|
10
|
-
message: string;
|
|
11
|
-
};
|
|
12
|
-
/** A Chromium-based browser on macOS, Windows or Linux; CONSTRUCTOR_BROWSER overrides. */
|
|
13
|
-
export declare function findBrowser(): string | undefined;
|
|
14
|
-
/**
|
|
15
|
-
* Opens the sign-in window, waits for the user to sign in (up to 15 min), saves the
|
|
16
|
-
* session cookies to `file`, and closes the window at once. Never returns a token.
|
|
17
|
-
* `opened` runs once the window is up.
|
|
18
|
-
*/
|
|
19
|
-
export declare function signIn(config: Pick<Config, "baseUrl" | "apiPath" | "requestTimeoutMs">, file: string, opened?: () => void): Promise<SignInResult>;
|
package/dist/signin.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
// Sign in to Constructor in a separate Chrome window and save the session.
|
|
2
|
-
// The window uses a fresh temporary profile: a session nothing else holds, so the MCP can
|
|
3
|
-
// renew it without logging out the user's own browser (a refresh token works only once).
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
7
|
-
import { homedir, tmpdir } from "node:os";
|
|
8
|
-
import { delimiter, join } from "node:path";
|
|
9
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
-
import { claims, saveSession, setCookies, SESSION_COOKIES } from "./session.js";
|
|
11
|
-
const SIGN_IN_MINUTES = 15;
|
|
12
|
-
/** A Chromium-based browser on macOS, Windows or Linux; CONSTRUCTOR_BROWSER overrides. */
|
|
13
|
-
export function findBrowser() {
|
|
14
|
-
if (process.env.CONSTRUCTOR_BROWSER)
|
|
15
|
-
return existsSync(process.env.CONSTRUCTOR_BROWSER)
|
|
16
|
-
? process.env.CONSTRUCTOR_BROWSER
|
|
17
|
-
: undefined;
|
|
18
|
-
const env = process.env;
|
|
19
|
-
const installed = process.platform === "win32"
|
|
20
|
-
? [env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA]
|
|
21
|
-
.filter((d) => !!d)
|
|
22
|
-
.flatMap((d) => [
|
|
23
|
-
join(d, "Google", "Chrome", "Application", "chrome.exe"),
|
|
24
|
-
join(d, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
25
|
-
join(d, "Chromium", "Application", "chrome.exe"),
|
|
26
|
-
join(d, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
27
|
-
])
|
|
28
|
-
: process.platform === "darwin"
|
|
29
|
-
? [
|
|
30
|
-
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
31
|
-
"Chromium.app/Contents/MacOS/Chromium",
|
|
32
|
-
"Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
33
|
-
"Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
34
|
-
].flatMap((app) => [
|
|
35
|
-
join("/Applications", app),
|
|
36
|
-
join(homedir(), "Applications", app),
|
|
37
|
-
])
|
|
38
|
-
: [];
|
|
39
|
-
// Then the PATH, as Linux installs them (and any OS where the user put one there).
|
|
40
|
-
const names = [
|
|
41
|
-
"google-chrome",
|
|
42
|
-
"google-chrome-stable",
|
|
43
|
-
"chromium",
|
|
44
|
-
"chromium-browser",
|
|
45
|
-
"microsoft-edge",
|
|
46
|
-
"brave-browser",
|
|
47
|
-
"chrome",
|
|
48
|
-
"msedge",
|
|
49
|
-
];
|
|
50
|
-
const exts = process.platform === "win32"
|
|
51
|
-
? (env.PATHEXT ?? ".EXE").split(";").filter(Boolean)
|
|
52
|
-
: [""];
|
|
53
|
-
const onPath = (env.PATH ?? "")
|
|
54
|
-
.split(delimiter)
|
|
55
|
-
.filter(Boolean)
|
|
56
|
-
.flatMap((dir) => names.flatMap((n) => exts.map((e) => join(dir, n + e.toLowerCase()))));
|
|
57
|
-
return [...installed, ...onPath].find((c) => existsSync(c));
|
|
58
|
-
}
|
|
59
|
-
/** Minimal Chrome DevTools Protocol client over the browser's WebSocket. */
|
|
60
|
-
async function devtools(url) {
|
|
61
|
-
const ws = new WebSocket(url);
|
|
62
|
-
await new Promise((ok, fail) => {
|
|
63
|
-
ws.onopen = ok;
|
|
64
|
-
ws.onerror = () => fail(new Error("Could not connect to the browser."));
|
|
65
|
-
});
|
|
66
|
-
let id = 0;
|
|
67
|
-
const waiting = new Map();
|
|
68
|
-
ws.onmessage = (e) => {
|
|
69
|
-
const m = JSON.parse(String(e.data));
|
|
70
|
-
waiting.get(m.id)?.(m);
|
|
71
|
-
waiting.delete(m.id);
|
|
72
|
-
};
|
|
73
|
-
return {
|
|
74
|
-
send(method, params = {}) {
|
|
75
|
-
return new Promise((ok, fail) => {
|
|
76
|
-
const n = ++id;
|
|
77
|
-
waiting.set(n, (m) => m.error ? fail(new Error(m.error.message)) : ok(m.result));
|
|
78
|
-
ws.send(JSON.stringify({ id: n, method, params }));
|
|
79
|
-
});
|
|
80
|
-
},
|
|
81
|
-
close: () => ws.close(),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Opens the sign-in window, waits for the user to sign in (up to 15 min), saves the
|
|
86
|
-
* session cookies to `file`, and closes the window at once. Never returns a token.
|
|
87
|
-
* `opened` runs once the window is up.
|
|
88
|
-
*/
|
|
89
|
-
export async function signIn(config, file, opened = () => { }) {
|
|
90
|
-
const fallback = `save a session from a copied cURL instead: npx -y -p constructor-research-mcp constructor-research-set-token <file with the cURL> --to ${file}`;
|
|
91
|
-
const headless = !!process.env.CONSTRUCTOR_LOGIN_HEADLESS;
|
|
92
|
-
if (process.platform === "linux" &&
|
|
93
|
-
!headless &&
|
|
94
|
-
!process.env.DISPLAY &&
|
|
95
|
-
!process.env.WAYLAND_DISPLAY)
|
|
96
|
-
return {
|
|
97
|
-
status: "sign_in_failed",
|
|
98
|
-
message: `This machine has no screen to open a sign-in window on (no DISPLAY or WAYLAND_DISPLAY). Sign in where the MCP has a desktop, or ${fallback}.`,
|
|
99
|
-
};
|
|
100
|
-
const exe = findBrowser();
|
|
101
|
-
if (!exe)
|
|
102
|
-
return {
|
|
103
|
-
status: "sign_in_failed",
|
|
104
|
-
message: `No Chrome, Edge, Chromium or Brave found. Set CONSTRUCTOR_BROWSER in the MCP settings to the browser's executable, or ${fallback}.`,
|
|
105
|
-
};
|
|
106
|
-
const host = new URL(config.baseUrl).hostname;
|
|
107
|
-
const profile = await mkdtemp(join(tmpdir(), "constructor-login-"));
|
|
108
|
-
const chrome = spawn(exe, [
|
|
109
|
-
`--user-data-dir=${profile}`,
|
|
110
|
-
"--remote-debugging-port=0",
|
|
111
|
-
"--remote-allow-origins=http://127.0.0.1",
|
|
112
|
-
"--no-first-run",
|
|
113
|
-
"--no-default-browser-check",
|
|
114
|
-
"--disable-sync",
|
|
115
|
-
// Tests run the browser without a window.
|
|
116
|
-
...(headless ? ["--headless=new"] : []),
|
|
117
|
-
"--new-window",
|
|
118
|
-
`${config.baseUrl}/research/`,
|
|
119
|
-
], { stdio: "ignore" });
|
|
120
|
-
let exited = false;
|
|
121
|
-
chrome.on("exit", () => (exited = true));
|
|
122
|
-
chrome.on("error", () => (exited = true));
|
|
123
|
-
const failed = (message) => ({
|
|
124
|
-
status: "sign_in_failed",
|
|
125
|
-
message,
|
|
126
|
-
});
|
|
127
|
-
try {
|
|
128
|
-
let port, path;
|
|
129
|
-
for (let i = 0; i < 300 && !port && !exited; i++) {
|
|
130
|
-
try {
|
|
131
|
-
[port, path] = (await readFile(join(profile, "DevToolsActivePort"), "utf8")).split("\n");
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
await delay(100);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (!port)
|
|
138
|
-
return failed(`The browser did not start (${exe}); ${fallback}.`);
|
|
139
|
-
const cdp = await devtools(`ws://127.0.0.1:${port}${path}`);
|
|
140
|
-
opened();
|
|
141
|
-
const session = async () => {
|
|
142
|
-
const { cookies } = await cdp.send("Storage.getCookies");
|
|
143
|
-
return new Map(cookies
|
|
144
|
-
.filter((c) => SESSION_COOKIES.includes(c.name) &&
|
|
145
|
-
(c.domain.replace(/^\./, "") === host ||
|
|
146
|
-
host.endsWith(c.domain.startsWith(".") ? c.domain : "." + c.domain)))
|
|
147
|
-
.map((c) => [c.name, c.value]));
|
|
148
|
-
};
|
|
149
|
-
const deadline = Date.now() + SIGN_IN_MINUTES * 60000;
|
|
150
|
-
let found;
|
|
151
|
-
while (!exited && Date.now() < deadline) {
|
|
152
|
-
const s = await session().catch(() => undefined);
|
|
153
|
-
const exp = claims(s?.get("ap_access_token"))?.exp;
|
|
154
|
-
if (s?.has("ap_refresh_token") && exp && exp * 1000 > Date.now()) {
|
|
155
|
-
// The page may still be loading; read again once it settles, then close at once so
|
|
156
|
-
// the window never renews this session itself.
|
|
157
|
-
await delay(3000);
|
|
158
|
-
found = (await session().catch(() => undefined)) ?? s;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
await delay(1000);
|
|
162
|
-
}
|
|
163
|
-
await cdp.send("Browser.close").catch(() => { });
|
|
164
|
-
cdp.close();
|
|
165
|
-
if (!found)
|
|
166
|
-
return failed(exited
|
|
167
|
-
? "The window was closed before sign-in finished; nothing was saved."
|
|
168
|
-
: `No sign-in within ${SIGN_IN_MINUTES} min; nothing was saved.`);
|
|
169
|
-
// Check the access token without sending the refresh token, so nothing is renewed.
|
|
170
|
-
const response = await fetch(config.baseUrl + config.apiPath + "/auth/session", {
|
|
171
|
-
headers: {
|
|
172
|
-
Cookie: `ap_access_token=${found.get("ap_access_token")}`,
|
|
173
|
-
Accept: "application/json",
|
|
174
|
-
},
|
|
175
|
-
redirect: "manual",
|
|
176
|
-
signal: AbortSignal.timeout(config.requestTimeoutMs),
|
|
177
|
-
}).catch(() => undefined);
|
|
178
|
-
await response?.body?.cancel();
|
|
179
|
-
if (response?.status === 401)
|
|
180
|
-
return failed("Constructor rejected the new session; nothing was saved.");
|
|
181
|
-
await saveSession(file, new Map([...found, ...(response ? setCookies(response) : [])]), config.baseUrl);
|
|
182
|
-
const end = claims(found.get("ap_access_token"))?.max_slt;
|
|
183
|
-
const until = end
|
|
184
|
-
? new Date(end * 1000).toISOString().slice(0, 16).replace("T", " ") +
|
|
185
|
-
" UTC"
|
|
186
|
-
: undefined;
|
|
187
|
-
return {
|
|
188
|
-
status: "signed_in",
|
|
189
|
-
message: `Signed in to ${config.baseUrl}. Session saved to ${file}` +
|
|
190
|
-
(until ? `; the MCP renews it automatically until ${until}.` : "."),
|
|
191
|
-
session_file: file,
|
|
192
|
-
host: config.baseUrl,
|
|
193
|
-
until,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
catch (e) {
|
|
197
|
-
return failed(`Sign-in failed: ${e?.message ?? e}. Nothing was saved.`);
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
if (!exited) {
|
|
201
|
-
chrome.kill();
|
|
202
|
-
for (let i = 0; i < 50 && !exited; i++)
|
|
203
|
-
await delay(100);
|
|
204
|
-
if (!exited)
|
|
205
|
-
chrome.kill("SIGKILL"); // TerminateProcess on Windows
|
|
206
|
-
}
|
|
207
|
-
await rm(profile, { recursive: true, force: true }).catch(() => { });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
File without changes
|