@threenative/core 0.2.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/LICENSE +21 -0
- package/README.md +55 -0
- package/capabilities.json +5292 -0
- package/dist/assets-kyoF7JlJ.d.ts +103 -0
- package/dist/audio-BFiGneTL.d.ts +156 -0
- package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
- package/dist/game-XGrTzapq.d.ts +1166 -0
- package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
- package/dist/hot.d.ts +20 -2
- package/dist/hot.js +14 -2
- package/dist/index.d.ts +2108 -55
- package/dist/index.js +15645 -2222
- package/dist/net.d.ts +65 -0
- package/dist/net.js +643 -0
- package/dist/playtest.d.ts +37 -4
- package/dist/playtest.js +246 -546
- package/dist/react.d.ts +177 -0
- package/dist/react.js +635 -0
- package/dist/renderer-C6hqZpoG.d.ts +770 -0
- package/dist/ui-layer.d.ts +306 -0
- package/dist/ui-layer.js +425 -0
- package/dist/world.d.ts +254 -0
- package/dist/world.js +2686 -0
- package/gpl/LICENSE.GPL +117 -0
- package/gpl/convert.py +192 -0
- package/gpl/recipes/_common.py +169 -0
- package/gpl/recipes/bake_ao.py +111 -0
- package/gpl/recipes/decimate.py +64 -0
- package/gpl/recipes/retarget.py +131 -0
- package/gpl/recipes/unwrap.py +71 -0
- package/mcp/assets.mjs +5 -0
- package/mcp/blender-server.mjs +632 -0
- package/mcp/blender.mjs +27 -0
- package/mcp/engine-server.mjs +501 -0
- package/mcp/engine.mjs +31 -0
- package/mcp/install.d.mts +37 -0
- package/mcp/install.mjs +145 -0
- package/mcp/launch.mjs +72 -0
- package/mcp/sculpt.mjs +5 -0
- package/mcp/servers.d.mts +34 -0
- package/mcp/servers.mjs +160 -0
- package/package.json +76 -6
- package/patches/three@0.185.1.patch +522 -0
- package/scripts/apply-three-patch.mjs +297 -0
- package/scripts/ensure-mcp.mjs +43 -0
- package/scripts/postinstall.mjs +6 -0
- package/dist/audio-CEAw0w5y.d.ts +0 -35
- package/dist/game-DRt1Qhq3.d.ts +0 -429
package/mcp/install.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CODEX_MCP_SERVERS, mergeMcpServers } from "./servers.mjs";
|
|
4
|
+
|
|
5
|
+
/** The project being installed into, or undefined when this install has no project to write to —
|
|
6
|
+
* a nested dependency install, or core's own workspace build. */
|
|
7
|
+
export function installTarget(environment = process.env, cwd = process.cwd()) {
|
|
8
|
+
const target = path.resolve(environment.INIT_CWD ?? cwd);
|
|
9
|
+
if (target.split(path.sep).includes("node_modules")) return undefined;
|
|
10
|
+
const manifestPath = path.join(target, "package.json");
|
|
11
|
+
if (!existsSync(manifestPath)) return undefined;
|
|
12
|
+
let manifest;
|
|
13
|
+
try {
|
|
14
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest))
|
|
19
|
+
return undefined;
|
|
20
|
+
if (manifest.name === "@threenative/core") return undefined;
|
|
21
|
+
const dependencyGroups = [
|
|
22
|
+
manifest.dependencies,
|
|
23
|
+
manifest.devDependencies,
|
|
24
|
+
manifest.optionalDependencies,
|
|
25
|
+
manifest.peerDependencies,
|
|
26
|
+
];
|
|
27
|
+
const declaresCore = dependencyGroups.some(
|
|
28
|
+
(dependencies) =>
|
|
29
|
+
typeof dependencies === "object" &&
|
|
30
|
+
dependencies !== null &&
|
|
31
|
+
Object.hasOwn(dependencies, "@threenative/core"),
|
|
32
|
+
);
|
|
33
|
+
return declaresCore ? target : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Every agent host that reads a **project-scoped** MCP config, and the file each one reads.
|
|
37
|
+
*
|
|
38
|
+
* Project-scoped is the whole admission rule. A host that only reads a machine-wide config
|
|
39
|
+
* (Windsurf, Cline, Amp, the JetBrains assistants) is deliberately absent: installing a library
|
|
40
|
+
* into one game must never edit a file that governs every other project on the machine. Those
|
|
41
|
+
* hosts are wired by hand, and `docs/architecture/` says so. */
|
|
42
|
+
export const MCP_HOSTS = Object.freeze([
|
|
43
|
+
Object.freeze({
|
|
44
|
+
id: "claude-code",
|
|
45
|
+
label: "Claude Code",
|
|
46
|
+
file: ".mcp.json",
|
|
47
|
+
format: "mcpServers",
|
|
48
|
+
}),
|
|
49
|
+
Object.freeze({ id: "codex", label: "Codex", file: ".codex/config.toml", format: "codex" }),
|
|
50
|
+
Object.freeze({ id: "cursor", label: "Cursor", file: ".cursor/mcp.json", format: "mcpServers" }),
|
|
51
|
+
Object.freeze({ id: "vscode", label: "VS Code", file: ".vscode/mcp.json", format: "vscode" }),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
id: "gemini-cli",
|
|
54
|
+
label: "Gemini CLI",
|
|
55
|
+
file: ".gemini/settings.json",
|
|
56
|
+
format: "mcpServers",
|
|
57
|
+
}),
|
|
58
|
+
Object.freeze({
|
|
59
|
+
id: "opencode",
|
|
60
|
+
label: "opencode",
|
|
61
|
+
file: "opencode.json",
|
|
62
|
+
format: "opencode",
|
|
63
|
+
// Written only when this installer creates the file, so a second install still reports
|
|
64
|
+
// "unchanged" rather than rewriting a config the user has since edited.
|
|
65
|
+
seed: Object.freeze({ $schema: "https://opencode.ai/config.json" }),
|
|
66
|
+
}),
|
|
67
|
+
Object.freeze({ id: "zed", label: "Zed", file: ".zed/settings.json", format: "zed" }),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/** Adds the ThreeNative servers to one host's JSON config, creating it when absent. Returns what it
|
|
71
|
+
* did so the installer can say so once and stay quiet otherwise. */
|
|
72
|
+
export function ensureJsonMcpConfig(target, file, format, seed = undefined) {
|
|
73
|
+
const configPath = path.join(target, file);
|
|
74
|
+
let existing;
|
|
75
|
+
if (existsSync(configPath)) {
|
|
76
|
+
// A config we cannot parse is still the user's. Rewriting it would drop servers we never wrote.
|
|
77
|
+
try {
|
|
78
|
+
existing = JSON.parse(readFileSync(configPath, "utf8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return "unreadable";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let merged;
|
|
84
|
+
try {
|
|
85
|
+
merged = mergeMcpServers(existing, format);
|
|
86
|
+
} catch {
|
|
87
|
+
return "unreadable";
|
|
88
|
+
}
|
|
89
|
+
const { changed, config, conflicts } = merged;
|
|
90
|
+
if (!changed) return conflicts.length > 0 ? "conflict" : "unchanged";
|
|
91
|
+
const written = existing === undefined ? { ...seed, ...config } : config;
|
|
92
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
93
|
+
writeFileSync(configPath, `${JSON.stringify(written, null, 2)}\n`);
|
|
94
|
+
return conflicts.length > 0 ? "conflict" : existing === undefined ? "created" : "updated";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Adds the ThreeNative servers to `<target>/.mcp.json`, the config Claude Code reads. */
|
|
98
|
+
export function ensureMcpConfig(target) {
|
|
99
|
+
return ensureJsonMcpConfig(target, ".mcp.json", "mcpServers");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Adds any missing ThreeNative servers to Codex's project-scoped MCP config without replacing
|
|
103
|
+
* user-authored settings or server definitions. TOML rather than JSON, so this one appends text
|
|
104
|
+
* instead of re-serialising: a config we cannot parse back is one we must not rewrite. */
|
|
105
|
+
export function ensureCodexMcpConfig(target) {
|
|
106
|
+
const directory = path.join(target, ".codex");
|
|
107
|
+
const configPath = path.join(directory, "config.toml");
|
|
108
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
109
|
+
const missing = CODEX_MCP_SERVERS.filter(
|
|
110
|
+
({ name }) => !existing.includes(`[mcp_servers.${name}]`),
|
|
111
|
+
);
|
|
112
|
+
if (missing.length === 0) return "unchanged";
|
|
113
|
+
mkdirSync(directory, { recursive: true });
|
|
114
|
+
const separator =
|
|
115
|
+
existing.length === 0 || existing.endsWith("\n\n")
|
|
116
|
+
? ""
|
|
117
|
+
: existing.endsWith("\n")
|
|
118
|
+
? "\n"
|
|
119
|
+
: "\n\n";
|
|
120
|
+
writeFileSync(
|
|
121
|
+
configPath,
|
|
122
|
+
`${existing}${separator}${missing.map(({ body }) => body).join("\n\n")}\n`,
|
|
123
|
+
);
|
|
124
|
+
return existing.length === 0 ? "created" : "updated";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Wires every host in `MCP_HOSTS`, mapping host id to what the write did. One host's unreadable
|
|
128
|
+
* or unwritable config never stops the rest: a project with a hand-edited `.cursor/mcp.json`
|
|
129
|
+
* should still get its Codex and VS Code tools. */
|
|
130
|
+
export function ensureHostMcpConfigs(target) {
|
|
131
|
+
const outcomes = new Map();
|
|
132
|
+
for (const host of MCP_HOSTS) {
|
|
133
|
+
try {
|
|
134
|
+
outcomes.set(
|
|
135
|
+
host.id,
|
|
136
|
+
host.format === "codex"
|
|
137
|
+
? ensureCodexMcpConfig(target)
|
|
138
|
+
: ensureJsonMcpConfig(target, host.file, host.format, host.seed),
|
|
139
|
+
);
|
|
140
|
+
} catch {
|
|
141
|
+
outcomes.set(host.id, "unreadable");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return outcomes;
|
|
145
|
+
}
|
package/mcp/launch.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** Where a package is installed, or undefined when it is not. Node's own resolver is the wrong tool
|
|
7
|
+
* here: `threenative-engine-mcp` publishes an `exports` map with only an `import` condition, which
|
|
8
|
+
* `createRequire().resolve` refuses, and the answer we want is the directory rather than a
|
|
9
|
+
* specifier's entry point. Walking `node_modules` upward from both the project and this package
|
|
10
|
+
* covers a direct dependency, a hoisted one, and pnpm's nested store alike. */
|
|
11
|
+
function packageDirectory(name) {
|
|
12
|
+
const starts = [process.cwd(), path.dirname(fileURLToPath(import.meta.url))];
|
|
13
|
+
for (const start of starts) {
|
|
14
|
+
let directory = path.resolve(start);
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = path.join(directory, "node_modules", name);
|
|
17
|
+
if (existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
18
|
+
const parent = path.dirname(directory);
|
|
19
|
+
if (parent === directory) break;
|
|
20
|
+
directory = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The module that starts the server. The published main entry is not always it —
|
|
27
|
+
* `threenative-sculpt-mcp` exports a library from `dist/index.js` and starts its server from the
|
|
28
|
+
* `bin` file — so `bin` wins wherever a package declares one. */
|
|
29
|
+
function serverEntry(directory) {
|
|
30
|
+
const manifest = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
|
|
31
|
+
const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[manifest.name];
|
|
32
|
+
const exported =
|
|
33
|
+
typeof manifest.exports === "string"
|
|
34
|
+
? manifest.exports
|
|
35
|
+
: (manifest.exports?.["."]?.import ?? manifest.exports?.["."]);
|
|
36
|
+
const relative =
|
|
37
|
+
bin ??
|
|
38
|
+
(typeof exported === "string" ? exported : undefined) ??
|
|
39
|
+
manifest.main ??
|
|
40
|
+
manifest.module;
|
|
41
|
+
return relative === undefined ? undefined : path.resolve(directory, relative);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Runs an MCP server the project may or may not have installed. Nothing here may write to stdout:
|
|
45
|
+
* that stream is the JSON-RPC transport, and one stray line makes the server unusable. */
|
|
46
|
+
export async function launchMcpServer({ name, version, env = {} }) {
|
|
47
|
+
for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
|
|
48
|
+
const directory = packageDirectory(name);
|
|
49
|
+
const entry = directory === undefined ? undefined : serverEntry(directory);
|
|
50
|
+
if (entry !== undefined && existsSync(entry)) {
|
|
51
|
+
// A server entry that is also a `bin` starts itself only when it is the process's main module,
|
|
52
|
+
// which it decides by comparing `process.argv[1]` against its own path. Importing it without
|
|
53
|
+
// this line loads the module and starts nothing, and the host reports a server that connected
|
|
54
|
+
// and then went silent.
|
|
55
|
+
process.argv[1] = entry;
|
|
56
|
+
await import(pathToFileURL(entry).href);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Not installed. `npx` keeps the server available in a project that never listed it, which is the
|
|
60
|
+
// whole point of wiring these in from the library rather than from a scaffold.
|
|
61
|
+
const child = spawn("npx", ["--yes", `${name}@${version}`], {
|
|
62
|
+
env: process.env,
|
|
63
|
+
stdio: "inherit",
|
|
64
|
+
});
|
|
65
|
+
child.on("error", (error) => {
|
|
66
|
+
process.stderr.write(`TN_MCP_LAUNCH: ${name} is not installed and npx failed: ${error}\n`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
child.on("exit", (code, signal) => {
|
|
70
|
+
process.exit(signal === null ? (code ?? 0) : 1);
|
|
71
|
+
});
|
|
72
|
+
}
|
package/mcp/sculpt.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Types for `servers.mjs`. That file is plain JavaScript by design — `@threenative/core`'s
|
|
2
|
+
// postinstall runs it before anything is built — so it can never be a `.ts`. Without this
|
|
3
|
+
// declaration every consumer reached for `@ts-expect-error` and then cast the result back into a
|
|
4
|
+
// shape of its own, which is three private copies of one contract.
|
|
5
|
+
|
|
6
|
+
export interface IMcpServerEntry {
|
|
7
|
+
readonly args: readonly string[];
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IMcpPackage {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One host's whole config with this format's server table merged in. The table's key is the
|
|
18
|
+
* host's own (`mcpServers`, `servers`, `mcp`, `context_servers`), so a caller reading a key it did
|
|
19
|
+
* not ask for gets `undefined` rather than a silently empty object. */
|
|
20
|
+
export interface IMcpConfigFormat {
|
|
21
|
+
readonly changed: boolean;
|
|
22
|
+
readonly config: Record<string, Record<string, Record<string, unknown>> | undefined>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ICodexMcpServer {
|
|
26
|
+
readonly body: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export declare const MCP_SERVERS: Readonly<Record<string, IMcpServerEntry>>;
|
|
31
|
+
export declare const MCP_PACKAGES: Readonly<Record<string, IMcpPackage>>;
|
|
32
|
+
export declare const SERVER_FORMAT_NAMES: readonly string[];
|
|
33
|
+
export declare const CODEX_MCP_SERVERS: readonly ICodexMcpServer[];
|
|
34
|
+
export declare function mergeMcpServers(existing: unknown, format?: string): IMcpConfigFormat;
|
package/mcp/servers.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// The MCP servers a ThreeNative project always has. `@threenative/core` is a direct dependency of
|
|
2
|
+
// every generated game, so `./node_modules/@threenative/core/mcp/<file>` is a path that resolves
|
|
3
|
+
// under npm, pnpm and yarn alike — unlike `./node_modules/threenative-asset-mcp/...`, which only
|
|
4
|
+
// exists when the package manager hoists, and which a project that never listed the server as a
|
|
5
|
+
// direct dependency does not have at all.
|
|
6
|
+
export const MCP_SERVERS = Object.freeze({
|
|
7
|
+
"threenative-assets": Object.freeze({
|
|
8
|
+
command: "node",
|
|
9
|
+
args: Object.freeze(["./node_modules/@threenative/core/mcp/assets.mjs"]),
|
|
10
|
+
// Entries rather than an object literal: these are environment variable names, not identifiers,
|
|
11
|
+
// and written as property names they read as names the naming rule must judge.
|
|
12
|
+
env: Object.freeze(
|
|
13
|
+
Object.fromEntries([
|
|
14
|
+
["ASSET_DOWNLOAD_DIR", "./public/assets"],
|
|
15
|
+
["AUDIO_DOWNLOAD_DIR", "./public/audio"],
|
|
16
|
+
]),
|
|
17
|
+
),
|
|
18
|
+
}),
|
|
19
|
+
"threenative-sculpt": Object.freeze({
|
|
20
|
+
command: "node",
|
|
21
|
+
args: Object.freeze(["./node_modules/@threenative/core/mcp/sculpt.mjs"]),
|
|
22
|
+
}),
|
|
23
|
+
"threenative-engine": Object.freeze({
|
|
24
|
+
command: "node",
|
|
25
|
+
args: Object.freeze(["./node_modules/@threenative/core/mcp/engine.mjs"]),
|
|
26
|
+
}),
|
|
27
|
+
// Drives an installed Blender headlessly so a downloaded .fbx becomes a GLB. It ships no
|
|
28
|
+
// Blender and probes for one only when a tool is called, so a project that never converts a
|
|
29
|
+
// model pays nothing for having this server wired.
|
|
30
|
+
"threenative-blender": Object.freeze({
|
|
31
|
+
command: "node",
|
|
32
|
+
args: Object.freeze(["./node_modules/@threenative/core/mcp/blender.mjs"]),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** The external package each shim launches. Core installs asset and sculpt transitively and bundles
|
|
37
|
+
* engine discovery itself: a game author must never know these package names or install them
|
|
38
|
+
* separately. The pinned engine version remains an emergency fallback for a legacy development
|
|
39
|
+
* checkout whose bundle has not been built. */
|
|
40
|
+
export const MCP_PACKAGES = Object.freeze({
|
|
41
|
+
assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.8.0" }),
|
|
42
|
+
sculpt: Object.freeze({ name: "threenative-sculpt-mcp", version: "0.1.1" }),
|
|
43
|
+
engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.1" }),
|
|
44
|
+
blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.1" }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** How one host spells a stdio MCP server. Hosts agree on what to run and disagree on where to
|
|
48
|
+
* write it down, so the servers are declared once above and translated here. `key` is the property
|
|
49
|
+
* the host reads its server table from; `entry` is one server in that host's own shape. */
|
|
50
|
+
const SERVER_FORMATS = Object.freeze({
|
|
51
|
+
// Claude Code (`.mcp.json`), Cursor and the Gemini CLI all read this one.
|
|
52
|
+
mcpServers: Object.freeze({
|
|
53
|
+
key: "mcpServers",
|
|
54
|
+
entry: (server) => ({
|
|
55
|
+
command: server.command,
|
|
56
|
+
args: [...server.args],
|
|
57
|
+
...(server.env === undefined ? {} : { env: { ...server.env } }),
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
// VS Code names the table `servers` and requires an explicit transport per server.
|
|
61
|
+
vscode: Object.freeze({
|
|
62
|
+
key: "servers",
|
|
63
|
+
entry: (server) => ({
|
|
64
|
+
type: "stdio",
|
|
65
|
+
command: server.command,
|
|
66
|
+
args: [...server.args],
|
|
67
|
+
...(server.env === undefined ? {} : { env: { ...server.env } }),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
// opencode folds the command and its arguments into one array and spells the environment
|
|
71
|
+
// `environment`. A server it does not mark enabled is written down and never started.
|
|
72
|
+
opencode: Object.freeze({
|
|
73
|
+
key: "mcp",
|
|
74
|
+
entry: (server) => ({
|
|
75
|
+
type: "local",
|
|
76
|
+
command: [server.command, ...server.args],
|
|
77
|
+
enabled: true,
|
|
78
|
+
...(server.env === undefined ? {} : { environment: { ...server.env } }),
|
|
79
|
+
}),
|
|
80
|
+
}),
|
|
81
|
+
// Zed calls them context servers and needs `source: "custom"` to accept a command of our own.
|
|
82
|
+
zed: Object.freeze({
|
|
83
|
+
key: "context_servers",
|
|
84
|
+
entry: (server) => ({
|
|
85
|
+
source: "custom",
|
|
86
|
+
command: server.command,
|
|
87
|
+
args: [...server.args],
|
|
88
|
+
...(server.env === undefined ? {} : { env: { ...server.env } }),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const SERVER_FORMAT_NAMES = Object.freeze(Object.keys(SERVER_FORMATS));
|
|
94
|
+
|
|
95
|
+
/** Adds the ThreeNative servers to an existing config without touching anything else in it, and
|
|
96
|
+
* reports whether the file needs rewriting. Fails closed on a config that is not an object: an
|
|
97
|
+
* unreadable config is the user's, and overwriting it would lose servers we did not write. */
|
|
98
|
+
export function mergeMcpServers(existing, format = "mcpServers") {
|
|
99
|
+
const shape = SERVER_FORMATS[format];
|
|
100
|
+
if (shape === undefined) throw new Error(`Unknown MCP config format '${format}'.`);
|
|
101
|
+
const malformed =
|
|
102
|
+
existing !== undefined &&
|
|
103
|
+
(typeof existing !== "object" || existing === null || Array.isArray(existing));
|
|
104
|
+
if (malformed) {
|
|
105
|
+
throw new Error("MCP config root must be an object.");
|
|
106
|
+
}
|
|
107
|
+
const previous = existing ?? {};
|
|
108
|
+
const existingServers = previous[shape.key];
|
|
109
|
+
if (
|
|
110
|
+
existingServers !== undefined &&
|
|
111
|
+
(typeof existingServers !== "object" ||
|
|
112
|
+
existingServers === null ||
|
|
113
|
+
Array.isArray(existingServers))
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(`MCP config '${shape.key}' must be an object.`);
|
|
116
|
+
}
|
|
117
|
+
const servers = { ...(existingServers ?? {}) };
|
|
118
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
119
|
+
if (typeof server !== "object" || server === null || Array.isArray(server)) {
|
|
120
|
+
throw new Error(`MCP server '${name}' in '${shape.key}' must be an object.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let changed = false;
|
|
124
|
+
const conflicts = [];
|
|
125
|
+
for (const [name, server] of Object.entries(MCP_SERVERS)) {
|
|
126
|
+
const wanted = shape.entry(server);
|
|
127
|
+
if (Object.hasOwn(servers, name)) {
|
|
128
|
+
if (JSON.stringify(servers[name]) !== JSON.stringify(wanted)) conflicts.push(name);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
servers[name] = wanted;
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
return { changed, conflicts, config: { ...previous, [shape.key]: servers } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One server as a Codex TOML table. Derived from `MCP_SERVERS` rather than written out beside it:
|
|
138
|
+
* two hand-maintained lists of the same three servers drift, and the drift is silent until an
|
|
139
|
+
* agent in Codex quietly has no asset tools. */
|
|
140
|
+
function codexServerBlock(name, server) {
|
|
141
|
+
const args = server.args.map((argument) => JSON.stringify(argument)).join(", ");
|
|
142
|
+
const lines = [
|
|
143
|
+
`[mcp_servers.${name}]`,
|
|
144
|
+
`command = ${JSON.stringify(server.command)}`,
|
|
145
|
+
`args = [${args}]`,
|
|
146
|
+
];
|
|
147
|
+
if (server.env !== undefined) {
|
|
148
|
+
lines.push("", `[mcp_servers.${name}.env]`);
|
|
149
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
150
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const CODEX_MCP_SERVERS = Object.freeze(
|
|
157
|
+
Object.entries(MCP_SERVERS).map(([name, server]) =>
|
|
158
|
+
Object.freeze({ body: codexServerBlock(name, server), name }),
|
|
159
|
+
),
|
|
160
|
+
);
|
package/package.json
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenative/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"license": "MIT AND GPL-2.0-or-later",
|
|
4
5
|
"description": "The small vanilla Three.js game runtime foundation.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"threenative",
|
|
8
|
+
"three",
|
|
9
|
+
"threejs",
|
|
10
|
+
"webgpu",
|
|
11
|
+
"game-engine",
|
|
12
|
+
"game-framework",
|
|
13
|
+
"gamedev",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/ThreeNativeHQ/threenative#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ThreeNativeHQ/threenative/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ThreeNativeHQ/threenative.git",
|
|
23
|
+
"directory": "packages/core"
|
|
24
|
+
},
|
|
5
25
|
"type": "module",
|
|
6
26
|
"files": [
|
|
7
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"mcp",
|
|
30
|
+
"gpl",
|
|
31
|
+
"scripts/apply-three-patch.mjs",
|
|
32
|
+
"scripts/ensure-mcp.mjs",
|
|
33
|
+
"scripts/postinstall.mjs",
|
|
34
|
+
"patches",
|
|
35
|
+
"capabilities.json",
|
|
36
|
+
"README.md"
|
|
8
37
|
],
|
|
9
38
|
"main": "./dist/index.js",
|
|
10
39
|
"module": "./dist/index.js",
|
|
@@ -22,6 +51,22 @@
|
|
|
22
51
|
"types": "./dist/hot.d.ts",
|
|
23
52
|
"import": "./dist/hot.js"
|
|
24
53
|
},
|
|
54
|
+
"./react": {
|
|
55
|
+
"types": "./dist/react.d.ts",
|
|
56
|
+
"import": "./dist/react.js"
|
|
57
|
+
},
|
|
58
|
+
"./ui-layer": {
|
|
59
|
+
"types": "./dist/ui-layer.d.ts",
|
|
60
|
+
"import": "./dist/ui-layer.js"
|
|
61
|
+
},
|
|
62
|
+
"./world": {
|
|
63
|
+
"types": "./dist/world.d.ts",
|
|
64
|
+
"import": "./dist/world.js"
|
|
65
|
+
},
|
|
66
|
+
"./net": {
|
|
67
|
+
"types": "./dist/net.d.ts",
|
|
68
|
+
"import": "./dist/net.js"
|
|
69
|
+
},
|
|
25
70
|
"./package.json": "./package.json"
|
|
26
71
|
},
|
|
27
72
|
"publishConfig": {
|
|
@@ -31,18 +76,43 @@
|
|
|
31
76
|
"dependencies": {
|
|
32
77
|
"three": "0.185.1",
|
|
33
78
|
"three-mesh-bvh": "0.9.14",
|
|
34
|
-
"
|
|
79
|
+
"threenative-asset-mcp": "0.8.0",
|
|
80
|
+
"threenative-sculpt-mcp": "0.1.1",
|
|
81
|
+
"zustand": "5.0.14"
|
|
35
82
|
},
|
|
36
83
|
"devDependencies": {
|
|
84
|
+
"@types/react": "19.2.0",
|
|
85
|
+
"@types/react-reconciler": "0.32.1",
|
|
37
86
|
"@types/three": "0.185.3",
|
|
38
87
|
"publint": "0.3.22",
|
|
88
|
+
"react": "19.2.0",
|
|
89
|
+
"react-reconciler": "0.33.0",
|
|
39
90
|
"tsup": "8.5.1",
|
|
40
91
|
"typescript": "5.9.3",
|
|
41
|
-
"@threenative/playtest": "0.
|
|
92
|
+
"@threenative/playtest": "0.3.1",
|
|
93
|
+
"threenative-engine-mcp": "0.2.1",
|
|
94
|
+
"threenative-blender-mcp": "0.1.1"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@threenative/playtest": ">=0.3.1 <0.4.0",
|
|
98
|
+
"react": ">=19.2.0 <20",
|
|
99
|
+
"react-reconciler": ">=0.33.0 <0.34"
|
|
100
|
+
},
|
|
101
|
+
"peerDependenciesMeta": {
|
|
102
|
+
"@threenative/playtest": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"react": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"react-reconciler": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
42
111
|
},
|
|
43
112
|
"scripts": {
|
|
44
|
-
"
|
|
113
|
+
"postinstall": "node ./scripts/postinstall.mjs",
|
|
114
|
+
"build": "node scripts/generate-version.mjs && tsup --config tsup.config.ts && node scripts/bundle-engine-mcp.mjs && node scripts/bundle-blender-mcp.mjs && publint",
|
|
45
115
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "
|
|
116
|
+
"test": "publint --strict"
|
|
47
117
|
}
|
|
48
118
|
}
|