@spawn-dock/create 0.2.0-canary.20260321111558.f5e5655 → 0.2.3

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 CHANGED
@@ -29,7 +29,6 @@ npx --yes github:SpawnDock/create#main --token <pairing-token> [project-dir]
29
29
  - `spawndock.config.json`
30
30
  - `.env.local`
31
31
  - `spawndock.dev-tunnel.json`
32
- - `opencode.json`
33
32
  - `public/tonconnect-manifest.json`
34
33
 
35
34
  ## Built-in Overlay
@@ -40,11 +39,20 @@ The package also ships a built-in TMA overlay and applies it after cloning
40
39
  - `spawndock/dev.mjs`
41
40
  - `spawndock/next.mjs`
42
41
  - `spawndock/tunnel.mjs`
42
+ - `spawndock/mcp.mjs`
43
+ - `opencode.json`
44
+ - `.mcp.json`
43
45
  - `next.config.ts`
44
46
  - `public/tonconnect-manifest.json`
45
47
  - patching project scripts and `@spawn-dock/dev-tunnel`
46
48
 
47
- Generated MCP config points to `<controlPlaneUrl>/mcp/sse`.
49
+ `spawndock/mcp.mjs` resolves `<controlPlaneUrl>/mcp/sse` from
50
+ `spawndock.config.json`.
51
+
52
+ - `opencode.json` is shipped by the template for OpenCode.
53
+ - `.mcp.json` is shipped by the template for Claude Code.
54
+ - if `codex` is installed locally, bootstrap also registers the same MCP server in
55
+ the global Codex MCP config automatically.
48
56
 
49
57
  ## Development
50
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spawn-dock/create",
3
- "version": "0.2.0-canary.20260321111558.f5e5655",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "SpawnDock bootstrap CLI for local TMA projects",
6
6
  "license": "ISC",
@@ -6,6 +6,7 @@ const formatSuccess = (summary) => [
6
6
  `SpawnDock project created at ${summary.projectDir}`,
7
7
  `Project: ${summary.projectName}`,
8
8
  `Preview URL: ${summary.previewOrigin}`,
9
+ `MCP ready for: ${summary.mcpAgents.join(", ")}`,
9
10
  `Run: cd "${summary.projectDir}" && npm run dev`,
10
11
  ].join("\n");
11
12
  const cliProgram = pipe(readCliOptions, Effect.flatMap((options) => options.token.length > 0
@@ -4,6 +4,7 @@ export const DEFAULT_CLAIM_PATH = "/v1/bootstrap/claim";
4
4
  export const DEFAULT_TEMPLATE_REPO = "https://github.com/SpawnDock/tma-project.git";
5
5
  export const DEFAULT_TEMPLATE_BRANCH = "master";
6
6
  export const TEMPLATE_ID = "nextjs-template";
7
+ export const DEFAULT_MCP_AGENTS = ["OpenCode", "Claude Code"];
7
8
  export const normalizeDisplayName = (value) => value
8
9
  .split(/[^a-zA-Z0-9]+/g)
9
10
  .filter(Boolean)
@@ -30,6 +31,17 @@ export const buildMcpServerUrl = (controlPlaneUrl) => {
30
31
  url.pathname = normalizedPath.length > 0 ? `${normalizedPath}/mcp/sse` : "/mcp/sse";
31
32
  return url.toString();
32
33
  };
34
+ export const buildCodexMcpCommandArgs = (mcpServerUrl) => [
35
+ "mcp",
36
+ "add",
37
+ "spawndock",
38
+ "--env",
39
+ `MCP_SERVER_URL=${mcpServerUrl}`,
40
+ "--",
41
+ "npx",
42
+ "-y",
43
+ "@spawn-dock/mcp",
44
+ ];
33
45
  export const buildTonConnectManifest = (context, claim) => `${JSON.stringify({
34
46
  url: claim.previewOrigin,
35
47
  name: context.projectName,
@@ -64,19 +76,6 @@ export const buildGeneratedFiles = (context, claim) => {
64
76
  SPAWNDOCK_PROJECT_SLUG: claim.projectSlug,
65
77
  SPAWNDOCK_ALLOWED_DEV_ORIGINS: claim.previewOrigin,
66
78
  };
67
- const opencode = {
68
- $schema: "https://opencode.ai/config.json",
69
- mcp: {
70
- spawndock: {
71
- type: "local",
72
- command: ["npx", "-y", "@spawn-dock/mcp"],
73
- enabled: true,
74
- environment: {
75
- MCP_SERVER_URL: mcpServerUrl,
76
- },
77
- },
78
- },
79
- };
80
79
  return [
81
80
  {
82
81
  path: "spawndock.config.json",
@@ -97,10 +96,6 @@ export const buildGeneratedFiles = (context, claim) => {
97
96
  port: claim.localPort,
98
97
  }, null, 2)}\n`,
99
98
  },
100
- {
101
- path: "opencode.json",
102
- content: `${JSON.stringify(opencode, null, 2)}\n`,
103
- },
104
99
  {
105
100
  path: "public/tonconnect-manifest.json",
106
101
  content: buildTonConnectManifest(context, claim),
@@ -118,6 +113,7 @@ export const patchPackageJsonContent = (input) => {
118
113
  packageJson.devDependencies = {
119
114
  ...(packageJson.devDependencies ?? {}),
120
115
  "@spawn-dock/dev-tunnel": "latest",
116
+ "@spawn-dock/mcp": "latest",
121
117
  };
122
118
  return `${JSON.stringify(packageJson, null, 2)}\n`;
123
119
  };
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { Effect } from "effect";
6
- import { buildGeneratedFiles, patchPackageJsonContent, resolveProjectContext, } from "../core/bootstrap.js";
6
+ import { buildCodexMcpCommandArgs, buildGeneratedFiles, buildMcpServerUrl, DEFAULT_MCP_AGENTS, patchPackageJsonContent, resolveProjectContext, } from "../core/bootstrap.js";
7
7
  const TEMPLATE_OVERLAY_DIR = resolve(fileURLToPath(new URL("../../../template-nextjs-overlay", import.meta.url)));
8
8
  export const bootstrapProject = (options) => Effect.gen(function* () {
9
9
  if (options.token.length === 0) {
@@ -24,10 +24,12 @@ export const bootstrapProject = (options) => Effect.gen(function* () {
24
24
  });
25
25
  yield* writeGeneratedFilesToProject(projectDir, context, claim);
26
26
  yield* installDependencies(projectDir);
27
+ const mcpAgents = yield* registerAgentIntegrations(projectDir, claim);
27
28
  return {
28
29
  projectDir,
29
30
  projectName: context.projectName,
30
31
  previewOrigin: claim.previewOrigin,
32
+ mcpAgents,
31
33
  };
32
34
  });
33
35
  const ensureEmptyProjectDir = (projectDir) => Effect.try({
@@ -120,6 +122,23 @@ const installDependencies = (projectDir) => Effect.gen(function* () {
120
122
  }
121
123
  yield* runCommand("pnpm", ["install"], projectDir);
122
124
  });
125
+ const registerAgentIntegrations = (projectDir, claim) => Effect.gen(function* () {
126
+ const integrations = [...DEFAULT_MCP_AGENTS];
127
+ const mcpServerUrl = buildMcpServerUrl(claim.controlPlaneUrl);
128
+ const codexRegistered = yield* registerCodexIntegration(projectDir, mcpServerUrl);
129
+ if (codexRegistered) {
130
+ integrations.push("Codex");
131
+ }
132
+ return integrations;
133
+ });
134
+ const registerCodexIntegration = (projectDir, mcpServerUrl) => Effect.gen(function* () {
135
+ const codexAvailable = yield* commandExists("codex");
136
+ if (!codexAvailable) {
137
+ return false;
138
+ }
139
+ const result = yield* runCommand("codex", buildCodexMcpCommandArgs(mcpServerUrl), projectDir, false);
140
+ return result.status === 0;
141
+ });
123
142
  const runCommand = (command, args, cwd = process.cwd(), failOnNonZero = true) => Effect.try({
124
143
  try: () => {
125
144
  const result = spawnSync(command, [...args], {
@@ -136,6 +155,23 @@ const runCommand = (command, args, cwd = process.cwd(), failOnNonZero = true) =>
136
155
  },
137
156
  catch: toError,
138
157
  });
158
+ const commandExists = (command) => Effect.try({
159
+ try: () => {
160
+ const result = spawnSync(command, ["--help"], {
161
+ cwd: process.cwd(),
162
+ encoding: "utf8",
163
+ stdio: "ignore",
164
+ });
165
+ if (result.error) {
166
+ if (isNodeError(result.error) && result.error.code === "ENOENT") {
167
+ return false;
168
+ }
169
+ throw toError(result.error);
170
+ }
171
+ return true;
172
+ },
173
+ catch: toError,
174
+ });
139
175
  const parseClaimResponse = (input, fallbackProjectSlug, fallbackControlPlaneUrl, fallbackLocalPort) => {
140
176
  if (!isRecord(input)) {
141
177
  return null;
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "spawndock": {
4
+ "type": "stdio",
5
+ "command": "node",
6
+ "args": [
7
+ "./spawndock/mcp.mjs"
8
+ ]
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "spawndock": {
5
+ "type": "local",
6
+ "command": [
7
+ "node",
8
+ "./spawndock/mcp.mjs"
9
+ ],
10
+ "enabled": true
11
+ }
12
+ }
13
+ }
@@ -12,3 +12,18 @@ export const resolvePreviewOrigin = (config) =>
12
12
 
13
13
  export const resolveAllowedDevOrigins = (config) =>
14
14
  [config.previewOrigin].filter(Boolean)
15
+
16
+ export const resolveMcpServerUrl = (config) => {
17
+ if (typeof config.mcpServerUrl === "string" && config.mcpServerUrl.length > 0) {
18
+ return config.mcpServerUrl
19
+ }
20
+
21
+ if (typeof config.controlPlaneUrl === "string" && config.controlPlaneUrl.length > 0) {
22
+ const url = new URL(config.controlPlaneUrl)
23
+ const normalizedPath = url.pathname.replace(/\/$/, "")
24
+ url.pathname = normalizedPath.length > 0 ? `${normalizedPath}/mcp/sse` : "/mcp/sse"
25
+ return url.toString()
26
+ }
27
+
28
+ throw new Error("Missing mcpServerUrl or controlPlaneUrl in spawndock.config.json")
29
+ }
@@ -0,0 +1,24 @@
1
+ import { spawn } from "node:child_process"
2
+
3
+ import { readSpawndockConfig, resolveMcpServerUrl } from "./config.mjs"
4
+
5
+ const config = readSpawndockConfig()
6
+ const mcpServerUrl = process.env.MCP_SERVER_URL ?? resolveMcpServerUrl(config)
7
+
8
+ const child = spawn("pnpm", ["exec", "spawn-dock-mcp"], {
9
+ cwd: process.cwd(),
10
+ env: {
11
+ ...process.env,
12
+ MCP_SERVER_URL: mcpServerUrl,
13
+ },
14
+ stdio: "inherit",
15
+ })
16
+
17
+ child.on("exit", (code) => {
18
+ process.exit(typeof code === "number" ? code : 1)
19
+ })
20
+
21
+ child.on("error", (error) => {
22
+ console.error(error)
23
+ process.exit(1)
24
+ })