constructor-research-mcp 0.3.0 → 0.3.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 +2 -2
- package/README.md +2 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/schemas.js +1 -1
- package/dist/server.js +4 -4
- package/dist/store.d.ts +6 -0
- package/dist/store.js +7 -1
- package/package.json +1 -1
package/INSTALL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Install the Constructor Research MCP
|
|
2
2
|
|
|
3
|
-
The MCP lets Claude Code, Codex or OpenCode deploy apps to Constructor Research: constructor.app, or your organisation's Constructor, such as
|
|
3
|
+
The MCP lets Claude Code, Codex or OpenCode deploy apps to Constructor Research: constructor.app, or your organisation's Constructor, such as acme.constructor.app. It runs on your machine as a local MCP server, installed from npm. Each tool signs in to Constructor once, with its own session.
|
|
4
4
|
|
|
5
5
|
## What you need
|
|
6
6
|
|
|
@@ -69,7 +69,7 @@ Do not put a token or cookie file in these settings; sign-in is handled by the M
|
|
|
69
69
|
## 2. Restart and sign in
|
|
70
70
|
|
|
71
71
|
1. Restart the tool, or run `/mcp` in Claude Code, so it starts the MCP. The first start downloads it and can take a minute.
|
|
72
|
-
2. Ask the agent to sign in. If your Constructor is not constructor.app, give its address, for example "sign in to Constructor at https://
|
|
72
|
+
2. Ask the agent to sign in. If your Constructor is not constructor.app, give its address, for example "sign in to Constructor at https://acme.constructor.app". A desk link from it works too.
|
|
73
73
|
3. The agent says a sign-in window will open and calls `sign_in`. A separate browser window opens at your Constructor's sign-in page.
|
|
74
74
|
4. Sign in. The window closes by itself.
|
|
75
75
|
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Each harness signs in once and keeps its own Constructor session; nothing is sto
|
|
|
40
40
|
2. The user signs in. The MCP reads the session cookies, closes the window itself and deletes the profile.
|
|
41
41
|
3. It saves `ap_access_token`, `ap_id_token` and `ap_refresh_token` to that harness's session file and answers `signed_in`. No token is ever returned or printed.
|
|
42
42
|
|
|
43
|
-
The MCP works with one Constructor at a time: the one the user signed in to, recorded as a `host=` line in the session file. Before the first sign-in it is `https://constructor.app`, or the host of a desk link the user gave. For another Constructor (for example `https://
|
|
43
|
+
The MCP works with one Constructor at a time: the one the user signed in to, recorded as a `host=` line in the session file. Before the first sign-in it is `https://constructor.app`, or the host of a desk link the user gave. For another Constructor (for example `https://acme.constructor.app`), the agent passes any link from it as `sign_in`'s `url`; the new sign-in replaces the old one, and the MCP works there from then on. A desk link from a different Constructor than the signed-in one returns `wrong_environment`, which tells the agent to sign in there with that link.
|
|
44
44
|
|
|
45
45
|
`sign_in` waits up to `wait_seconds` (default 20, so it stays within short harness tool timeouts); while the user is still signing in it returns `sign_in_pending`, and calling it again waits on the same window. With a renewable session already saved it answers `signed_in` without opening anything (`force=true` signs in again).
|
|
46
46
|
|
|
@@ -52,7 +52,7 @@ The same sign-in from a terminal (use `codex` or `opencode` in place of `claude-
|
|
|
52
52
|
|
|
53
53
|
```sh
|
|
54
54
|
npx -y -p constructor-research-mcp constructor-research-login --for claude-code
|
|
55
|
-
npx -y -p constructor-research-mcp constructor-research-login --url https://
|
|
55
|
+
npx -y -p constructor-research-mcp constructor-research-login --url https://acme.constructor.app --for claude-code
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
On a machine without a screen (Linux without `DISPLAY`/`WAYLAND_DISPLAY`), `sign_in` says so; save a session from a copied cURL (DevTools → Network → Copy as cURL) instead:
|
package/dist/config.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export declare function sessionArgs(args: string[], config: Config): {
|
|
|
40
40
|
};
|
|
41
41
|
/** Shell command that waits for deployments, carrying the settings that differ from the defaults. */
|
|
42
42
|
export declare function waitCommand(config: Config, ids: string[]): string;
|
|
43
|
+
/** This package's version, as published; the server reports it on connect. */
|
|
44
|
+
export declare const VERSION: string;
|
|
43
45
|
/**
|
|
44
46
|
* How to run the wait CLI of this very version. Run through npx, the MCP lives in npx's
|
|
45
47
|
* cache, whose folder npm may replace at any time, so the hint names the package instead.
|
package/dist/config.js
CHANGED
|
@@ -114,6 +114,8 @@ export function waitCommand(config, ids) {
|
|
|
114
114
|
.join("");
|
|
115
115
|
return `${env}${waitProgram()} ${ids.join(" ")}`;
|
|
116
116
|
}
|
|
117
|
+
/** This package's version, as published; the server reports it on connect. */
|
|
118
|
+
export const VERSION = JSON.parse(readFileSync(join(fileURLToPath(import.meta.url), "..", "..", "package.json"), "utf8")).version;
|
|
117
119
|
/**
|
|
118
120
|
* How to run the wait CLI of this very version. Run through npx, the MCP lives in npx's
|
|
119
121
|
* cache, whose folder npm may replace at any time, so the hint names the package instead.
|
package/dist/schemas.js
CHANGED
|
@@ -262,7 +262,7 @@ export const signInInput = z.strictObject({
|
|
|
262
262
|
.string()
|
|
263
263
|
.max(2000)
|
|
264
264
|
.optional()
|
|
265
|
-
.describe("Any link to the user's Constructor: a desk link, or just its address, e.g. https://
|
|
265
|
+
.describe("Any link to the user's Constructor: a desk link, or just its address, e.g. https://acme.constructor.app. The user signs in there and the MCP works there afterwards, replacing any earlier sign-in. Pass it when the user names their Constructor or gives a link; omit it to sign in where the MCP works now (https://constructor.app unless the user signed in elsewhere)."),
|
|
266
266
|
wait_seconds: z
|
|
267
267
|
.number()
|
|
268
268
|
.int()
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { configFromEnv, originOf } from "./config.js";
|
|
4
|
+
import { configFromEnv, originOf, VERSION } from "./config.js";
|
|
5
5
|
import { PlatformApi, ToolError } from "./api.js";
|
|
6
6
|
import { StateStore } from "./store.js";
|
|
7
7
|
import { DeploymentEngine, terminal } from "./deployment.js";
|
|
@@ -117,8 +117,8 @@ async function main() {
|
|
|
117
117
|
}
|
|
118
118
|
: result;
|
|
119
119
|
};
|
|
120
|
-
const server = new McpServer({ name: "constructor-research", version:
|
|
121
|
-
instructions: `Sign-in: the MCP works with one Constructor, https://constructor.app unless the user signed in to another. When the user asks to sign in, or names their Constructor or gives a link from it (e.g. https://
|
|
120
|
+
const server = new McpServer({ name: "constructor-research", version: VERSION }, {
|
|
121
|
+
instructions: `Sign-in: the MCP works with one Constructor, https://constructor.app unless the user signed in to another. When the user asks to sign in, or names their Constructor or gives a link from it (e.g. https://acme.constructor.app), call sign_in with url set to that link. Where to act: ask the user for the Constructor desk link (they open the desk in the browser and copy the address; it contains /projects/<project>/desks/<desk>) and pass it as desk_url. Do not pick a project or desk yourself; if the user has not said where to deploy or where files live, ask. Ask the user only before replacing, renaming or deleting files on a disk, once per request for the whole list (the tools return confirm_required); creating folders and uploading new files need no question. ${RULES} For a local app, pass build.local_path (the folder with the Dockerfile) and deploy_app uploads it first. Hardware: leave it out; the default is used. Set hardware minimums only when the app cannot run without a GPU or a lot of RAM. Use deploy_app/update_app once for the WHOLE application. The MCP builds, resolves service URLs, configures and starts services in the background. IDs are automatic. To wait, call get_deployment(wait_seconds=20) again as often as needed (a loop of such calls is fine); never perform the deployment steps yourself. On failure, tell the user the failed stage/service; failure.log_tail and get_logs show the last build or service log lines. Do not use browser or debugging tools. For source files: sync_files to the desk before building (the MCP checks each Dockerfile on the disk and reports warnings); after a needs_upload pause, sync and call get_deployment with files_uploaded=true. Started is not readiness.`,
|
|
122
122
|
});
|
|
123
123
|
const register = (name, description, schema, fn, readOnly = false, budget = 20000, scope) => {
|
|
124
124
|
if (scope && scope !== "deploy")
|
|
@@ -228,7 +228,7 @@ async function main() {
|
|
|
228
228
|
register("stop_app", "Stop all or selected services as one operation. MCP handles all requests. Waits up to wait_seconds (default 20). Stopped means stop requests accepted, not verified teardown.", lifecycleInput, (a) => runtime.lifecycle("stop", a), false, 20000, "project");
|
|
229
229
|
register("get_deployment", "With deployment_id, observe/wait for background execution; the MCP owns execution. Each call waits up to wait_seconds (max 20); to wait longer, call again, as often as needed (a loop of get_deployment calls is fine). Services carry status_changes and restarts_seen when the MCP saw the status change or a new container start. After sync_files (or manual upload) set files_uploaded=true; after cookie replacement checking the job resumes it. With no arguments, returns the map of all deployments: every deployment-enabled project with its applications and services (names, native statuses, public URLs, ports). With desk_url or project_id, returns that project's desks, images, hardware and applications; add application_id for one application. resume=false observes only. Never returns environment-variable values.", getInput, (a) => runtime.get(a), false, 20000, "deploy");
|
|
230
230
|
let signing;
|
|
231
|
-
register("sign_in", "Signs the user in to their Constructor: https://constructor.app unless url names another one (any link from it, e.g. https://
|
|
231
|
+
register("sign_in", "Signs the user in to their Constructor: https://constructor.app unless url names another one (any link from it, e.g. https://acme.constructor.app). Call it when the user asks to sign in, or when a tool returns needs_auth, after telling the user a sign-in window is about to open. It opens a separate Chrome window at the sign-in page; the user signs in there and the window closes by itself. There is one sign-in at a time: signing in to another Constructor replaces it, and the MCP works there afterwards. The session is kept for this harness only, and the MCP renews it automatically for up to 14 days. Waits up to wait_seconds; while the user is still signing in it returns sign_in_pending: call it again (no second window opens), then repeat the call that needed auth.", signInInput, async (a) => {
|
|
232
232
|
const i = signInInput.parse(a);
|
|
233
233
|
if (!signing) {
|
|
234
234
|
let host = config.baseUrl;
|
package/dist/store.d.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface Operation {
|
|
|
64
64
|
export declare class StateStore {
|
|
65
65
|
readonly config: Config;
|
|
66
66
|
constructor(config: Config);
|
|
67
|
+
/**
|
|
68
|
+
* How long a call waits for the lock. Every MCP process on this machine shares it, and
|
|
69
|
+
* a deployment step holds it for its API calls (seconds), so failing at once made other
|
|
70
|
+
* harnesses' calls "busy" about half the time while anything was deploying.
|
|
71
|
+
*/
|
|
72
|
+
lockWaitMs: number;
|
|
67
73
|
/** The Constructor API saved operations belong to; follows a sign-in to another host. */
|
|
68
74
|
get endpoint(): string;
|
|
69
75
|
private init;
|
package/dist/store.js
CHANGED
|
@@ -7,6 +7,12 @@ export class StateStore {
|
|
|
7
7
|
constructor(config) {
|
|
8
8
|
this.config = config;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* How long a call waits for the lock. Every MCP process on this machine shares it, and
|
|
12
|
+
* a deployment step holds it for its API calls (seconds), so failing at once made other
|
|
13
|
+
* harnesses' calls "busy" about half the time while anything was deploying.
|
|
14
|
+
*/
|
|
15
|
+
lockWaitMs = 15000;
|
|
10
16
|
/** The Constructor API saved operations belong to; follows a sign-in to another host. */
|
|
11
17
|
get endpoint() {
|
|
12
18
|
return this.config.baseUrl + this.config.apiPath;
|
|
@@ -70,7 +76,7 @@ export class StateStore {
|
|
|
70
76
|
async exclusive(fn) {
|
|
71
77
|
await this.init();
|
|
72
78
|
const lock = join(this.config.stateDir, ".lock");
|
|
73
|
-
return withLock(lock, fn,
|
|
79
|
+
return withLock(lock, fn, this.lockWaitMs, () => new ToolError("busy", `Another operation still holds ${lock} after ${this.lockWaitMs / 1000} s. Repeat the same call.`));
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
/**
|