@cmpose/mcp 0.1.1 → 0.2.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/README.md +103 -1
- package/package.json +12 -4
- package/src/gameGenerate.js +77 -0
- package/src/generate.js +1 -1
- package/src/server.js +56 -12
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
1
|
# @cmpose/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ask your AI agent for a **Compose Multiplatform** app and get a real, buildable Android + iOS
|
|
4
|
+
project on disk, generated locally from a tested template. It also generates **Unity** mobile
|
|
5
|
+
games (Android + iOS). This is a local stdio MCP server with four tools: `cmp_describe`,
|
|
6
|
+
`cmp_generate`, `game_describe` and `game_generate`. It is the same generator that runs
|
|
7
|
+
[cmpose.dev](https://cmpose.dev), with no account, no API key and no upload.
|
|
8
|
+
|
|
9
|
+
> "Create a blank Compose Multiplatform app called Habits, package com.acme.habits, in
|
|
10
|
+
> ~/Projects, with Room, light/dark theming and AdMob ads."
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Requires Node.js 20+ and git.
|
|
15
|
+
|
|
16
|
+
**Claude Code**
|
|
17
|
+
|
|
18
|
+
claude mcp add cmpose -- npx -y @cmpose/mcp
|
|
19
|
+
|
|
20
|
+
**Claude Desktop, Cursor, Windsurf and other MCP clients** (`mcpServers` config):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"cmpose": { "command": "npx", "args": ["-y", "@cmpose/mcp"] }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**VS Code** (`.vscode/mcp.json`):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"servers": {
|
|
35
|
+
"cmpose": { "type": "stdio", "command": "npx", "args": ["-y", "@cmpose/mcp"] }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## What you get
|
|
41
|
+
|
|
42
|
+
Every project has `androidApp`, `iosApp` (Xcode project), a `shared` module with Koin and
|
|
43
|
+
Navigation, logging (Kermit), image loading (Coil), in-app review, update check, an About
|
|
44
|
+
screen, a `build-logic` with convention plugins, and a README with a release checklist.
|
|
45
|
+
|
|
46
|
+
- **Template:** `showcase` (home, onboarding and paywall demo screens) or `blank`
|
|
47
|
+
(a clean shell with a sample feature).
|
|
48
|
+
- **Options for `blank`:** Ktor networking (with an optional in-app network inspector),
|
|
49
|
+
theming (light/dark/system), multi-language strings, DataStore, Room (KMP, bundled SQLite),
|
|
50
|
+
RevenueCat purchases with a custom Compose paywall, AdMob ads with UMP/ATT consent, detekt.
|
|
51
|
+
- **Your own feature modules:** up to 3, each with contract/data/domain/presentation layers.
|
|
52
|
+
- **CI:** an optional GitHub Actions workflow.
|
|
53
|
+
- **Your names everywhere:** package, bundle id, app name, min SDK and iOS deployment target.
|
|
54
|
+
|
|
55
|
+
## Games (Unity)
|
|
56
|
+
|
|
57
|
+
> "Create a 2D Unity game called SkyHopper, package com.acme.skyhopper, in ~/Projects, blank,
|
|
58
|
+
> with ads and leaderboards"
|
|
59
|
+
|
|
60
|
+
Every game project is a Unity 6 project (editor 6000.3.25f1, URP) for Android and iOS, with
|
|
61
|
+
VContainer + UniTask, a uGUI interface, a Boot → Menu → Game scene flow, a settings screen,
|
|
62
|
+
saving, deep links, and in-app review and update check.
|
|
63
|
+
|
|
64
|
+
- **Template:** `showcase` (a sample game with every flag on: a 2D runner, or a 3D track with
|
|
65
|
+
`render: "3d"`) or `blank` (an empty game with the flags you pick). `render` is `2d`
|
|
66
|
+
(default) or `3d`.
|
|
67
|
+
- **Flags for `blank`:** `saveSystem`, `uiTheme`, `multiLang`, `network`, `networkInspector`
|
|
68
|
+
(needs `network`, added for you), `purchases` (RevenueCat), `ads` (AdMob) and
|
|
69
|
+
`leaderboards` (Google Play Games and Game Center).
|
|
70
|
+
- **Your own feature modules (`blank`):** up to 3.
|
|
71
|
+
- **IDs:** `ids` takes your real AdMob (`admob`), RevenueCat (`revenueCat`) and Play Games +
|
|
72
|
+
Game Center (`gameServices`) IDs, each group with its flag; `game_describe` lists the fields.
|
|
73
|
+
A group you leave out keeps the test IDs / placeholders and is reported as a warning.
|
|
74
|
+
- **CI:** an optional GitHub Actions workflow with GameCI.
|
|
75
|
+
- **Your names everywhere:** package (application identifier), company name, app name, min SDK
|
|
76
|
+
(25+) and iOS deployment target (15.0+).
|
|
77
|
+
|
|
78
|
+
## Tools
|
|
79
|
+
|
|
80
|
+
- `cmp_describe`: the defaults, the options and the rules (name formats, limits, which option
|
|
81
|
+
needs which). Agents call it first.
|
|
82
|
+
- `cmp_generate`: creates the project. Required: `projectName` (letters only), `appName`,
|
|
83
|
+
`packageName` (e.g. `com.acme.habits`) and an absolute `outputDir`. Optional:
|
|
84
|
+
`templateType`, `features`, `featuresConfig`, `minSdk`, `iosVersion`, `ci`, `templateRef`.
|
|
85
|
+
It returns the project path, the template version it used, warnings (e.g. test ad IDs to
|
|
86
|
+
replace) and next steps.
|
|
87
|
+
- `game_describe`: the Unity game generator's engine and editor version, template types,
|
|
88
|
+
renders, flags and their dependencies, ID groups, defaults and limits. Agents call it first.
|
|
89
|
+
- `game_generate`: creates the game. Required: `projectName` (letters or digits, starting with
|
|
90
|
+
a letter), `appName`, `packageName` (e.g. `com.acme.skyhopper`) and an absolute `outputDir`.
|
|
91
|
+
Optional: `companyName`, `templateType`, `render`, `flags`, `features`, `minSdk`,
|
|
92
|
+
`iosVersion`, `ci`, `ids`, `templateRef`. It never overwrites a non-empty folder.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
Generation runs entirely on your machine. The first call downloads the pinned template
|
|
97
|
+
([Keremturker/cmpose-template](https://github.com/Keremturker/cmpose-template)) into
|
|
98
|
+
`~/.cache/cmpose` (override with `CMPOSE_CACHE_DIR`), and later calls reuse it. Games come the
|
|
99
|
+
same way from the pinned game template
|
|
100
|
+
([Keremturker/cmpose-game-template](https://github.com/Keremturker/cmpose-game-template)); Unity
|
|
101
|
+
never runs during generation. Each release pins one tag of each template, so `@cmpose/mcp@x.y.z`
|
|
102
|
+
always generates the same project.
|
|
103
|
+
|
|
104
|
+
Source and issues: [github.com/Keremturker/cmpose](https://github.com/Keremturker/cmpose).
|
|
105
|
+
MIT licensed. The projects you generate are yours.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmpose/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"mcpName": "io.github.Keremturker/cmpose",
|
|
5
|
+
"description": "MCP server that generates buildable Compose Multiplatform (Android + iOS) projects and Unity game projects locally from any AI agent",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -19,7 +20,14 @@
|
|
|
19
20
|
"kotlin-multiplatform",
|
|
20
21
|
"kmp",
|
|
21
22
|
"generator",
|
|
22
|
-
"scaffold"
|
|
23
|
+
"scaffold",
|
|
24
|
+
"android",
|
|
25
|
+
"ios",
|
|
26
|
+
"compose",
|
|
27
|
+
"unity",
|
|
28
|
+
"game",
|
|
29
|
+
"ai-agent",
|
|
30
|
+
"mcp-server"
|
|
23
31
|
],
|
|
24
32
|
"bin": {
|
|
25
33
|
"cmpose-mcp": "src/server.js"
|
|
@@ -35,7 +43,7 @@
|
|
|
35
43
|
"test": "node --test test/*.test.js"
|
|
36
44
|
},
|
|
37
45
|
"dependencies": {
|
|
38
|
-
"@cmpose/core": "0.
|
|
46
|
+
"@cmpose/core": "0.2.0",
|
|
39
47
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
40
48
|
"fs-extra": "^11.3.2",
|
|
41
49
|
"zod": "3.25.76"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const core = require('@cmpose/core');
|
|
6
|
+
const { removeEmptyDir, targetProblem } = require('./generate');
|
|
7
|
+
|
|
8
|
+
const fail = (text) => ({ isError: true, content: [{ type: 'text', text }] });
|
|
9
|
+
|
|
10
|
+
const NEXT_STEPS_GAME = [
|
|
11
|
+
'Open the folder in Unity Hub with editor 6000.3.25f1 (Android and iOS Build Support modules).',
|
|
12
|
+
'Press Play in the Boot scene, or build with File > Build Profiles.',
|
|
13
|
+
'Read README.md for signing, IDs and the release checklist.'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Validate → template cache → generate in a hidden sibling → rename into place.
|
|
17
|
+
// Never overwrites anything and never leaves half a project behind.
|
|
18
|
+
async function runGameGenerate(args, { cacheDir, remoteUrl, onProgress = () => { }, generatorVersion = null, now } = {}) {
|
|
19
|
+
const { outputDir, templateRef, ...rest } = args || {};
|
|
20
|
+
if (typeof outputDir !== 'string' || !path.isAbsolute(outputDir)) {
|
|
21
|
+
return fail('outputDir must be an absolute path (for example /path/to/projects).');
|
|
22
|
+
}
|
|
23
|
+
const checked = core.game.validateGame(rest);
|
|
24
|
+
if (!checked.ok) return fail(checked.error);
|
|
25
|
+
const value = checked.value;
|
|
26
|
+
const target = path.join(outputDir, value.projectName);
|
|
27
|
+
const problem = await targetProblem(target);
|
|
28
|
+
if (problem) return fail(problem);
|
|
29
|
+
|
|
30
|
+
const template = { repo: core.pins.game.repo, ref: templateRef || core.pins.game.ref, cacheDir };
|
|
31
|
+
let cache;
|
|
32
|
+
try {
|
|
33
|
+
const cached = await core.templateCache.isCached(template);
|
|
34
|
+
onProgress(cached ? 'Preparing the template' : 'Downloading the template (first run only)');
|
|
35
|
+
cache = await core.templateCache.ensure({ ...template, remoteUrl });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err instanceof core.TemplateFetchError || err instanceof TypeError) return fail(err.message);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await fs.ensureDir(outputDir);
|
|
42
|
+
const work = path.join(outputDir, `.cmpose-${crypto.randomBytes(6).toString('hex')}`);
|
|
43
|
+
try {
|
|
44
|
+
onProgress('Generating the project');
|
|
45
|
+
await fs.copy(cache.dir, work);
|
|
46
|
+
const source = { repo: core.pins.game.repo, ref: cache.ref, commit: cache.commit };
|
|
47
|
+
const summary = await core.game.generateGame(value, {
|
|
48
|
+
projectDir: work, template: source, coreVersion: core.version, generatorVersion, ...(now ? { now } : {})
|
|
49
|
+
});
|
|
50
|
+
const late = await targetProblem(target);
|
|
51
|
+
if (late) throw new Error(late);
|
|
52
|
+
await removeEmptyDir(target); // never recursive: a concurrent run's project stays intact
|
|
53
|
+
await fs.rename(work, target);
|
|
54
|
+
const result = {
|
|
55
|
+
projectDir: target,
|
|
56
|
+
template: source,
|
|
57
|
+
templateType: summary.templateType,
|
|
58
|
+
render: summary.render,
|
|
59
|
+
flags: summary.flags,
|
|
60
|
+
features: summary.features,
|
|
61
|
+
warnings: summary.warnings,
|
|
62
|
+
nextSteps: NEXT_STEPS_GAME
|
|
63
|
+
};
|
|
64
|
+
const text = [
|
|
65
|
+
`Generated ${value.projectName} at ${target}`,
|
|
66
|
+
`Template: ${source.repo}@${source.ref} (${String(source.commit).slice(0, 7)})`,
|
|
67
|
+
...result.warnings.map((w) => `Warning: ${w}`),
|
|
68
|
+
'Next steps:', ...NEXT_STEPS_GAME.map((s) => `- ${s}`)
|
|
69
|
+
].join('\n');
|
|
70
|
+
return { content: [{ type: 'text', text }], structuredContent: result };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
await fs.remove(work).catch(() => { });
|
|
73
|
+
return fail(`Generation failed: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { runGameGenerate, NEXT_STEPS_GAME };
|
package/src/generate.js
CHANGED
package/src/server.js
CHANGED
|
@@ -7,11 +7,31 @@ const core = require('@cmpose/core');
|
|
|
7
7
|
const pkg = require('../package.json');
|
|
8
8
|
const { describe } = require('./describe');
|
|
9
9
|
const { runGenerate } = require('./generate');
|
|
10
|
+
const { runGameGenerate } = require('./gameGenerate');
|
|
10
11
|
|
|
11
12
|
const GENERATE_DESCRIPTION = 'Generate a Compose Multiplatform (Android + iOS) project on this machine. ' +
|
|
12
13
|
'Call cmp_describe first for the options, defaults and validation rules. ' +
|
|
13
14
|
'Writes <outputDir>/<projectName>; never overwrites an existing non-empty folder.';
|
|
14
15
|
|
|
16
|
+
const GAME_GENERATE_DESCRIPTION = 'Generate a Unity 6 (Android + iOS) mobile game project on this machine. ' +
|
|
17
|
+
'Call game_describe first for the flags, defaults and validation rules. ' +
|
|
18
|
+
'showcase has every flag on; flags and features apply to blank only. ' +
|
|
19
|
+
'Writes <outputDir>/<projectName>; never overwrites an existing non-empty folder.';
|
|
20
|
+
|
|
21
|
+
// MCP progress notifications for a tool call that sent a progress token
|
|
22
|
+
function progressReporter(extra, total) {
|
|
23
|
+
const progressToken = extra && extra._meta && extra._meta.progressToken;
|
|
24
|
+
let step = 0;
|
|
25
|
+
return (message) => {
|
|
26
|
+
if (progressToken === undefined) return;
|
|
27
|
+
step += 1;
|
|
28
|
+
extra.sendNotification({
|
|
29
|
+
method: 'notifications/progress',
|
|
30
|
+
params: { progressToken, progress: step, total, message }
|
|
31
|
+
}).catch(() => { });
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
function createServer({ cacheDir, remoteUrl } = {}) {
|
|
16
36
|
const server = new McpServer({ name: 'cmpose', version: pkg.version });
|
|
17
37
|
|
|
@@ -39,20 +59,44 @@ function createServer({ cacheDir, remoteUrl } = {}) {
|
|
|
39
59
|
ci: z.boolean().optional(),
|
|
40
60
|
templateRef: z.string().optional().describe('Override the pinned template tag')
|
|
41
61
|
}
|
|
42
|
-
}, async (args, extra) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
return runGenerate(args, { cacheDir, remoteUrl, onProgress, generatorVersion: `mcp@${pkg.version}` });
|
|
62
|
+
}, async (args, extra) => runGenerate(args, {
|
|
63
|
+
cacheDir, remoteUrl, onProgress: progressReporter(extra, 2), generatorVersion: `mcp@${pkg.version}`
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
server.registerTool('game_describe', {
|
|
67
|
+
description: 'Describe the Unity game generator: engine and editor version, template types, renders, flags and their dependencies, ID groups, defaults, validation limits and the pinned game template.',
|
|
68
|
+
inputSchema: {}
|
|
69
|
+
}, async () => {
|
|
70
|
+
const facts = core.game.describeGame();
|
|
71
|
+
return { content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }], structuredContent: facts };
|
|
54
72
|
});
|
|
55
73
|
|
|
74
|
+
server.registerTool('game_generate', {
|
|
75
|
+
description: GAME_GENERATE_DESCRIPTION,
|
|
76
|
+
inputSchema: {
|
|
77
|
+
projectName: z.string().describe('Letters or digits, starting with a letter, no spaces (e.g. SkyHopper)'),
|
|
78
|
+
appName: z.string().describe('Display name (e.g. Sky Hopper)'),
|
|
79
|
+
packageName: z.string().describe('Reverse-domain application identifier (e.g. com.example.skyhopper)'),
|
|
80
|
+
outputDir: z.string().describe('Absolute path of the folder to create the project in'),
|
|
81
|
+
companyName: z.string().optional().describe('Company name (defaults to the second packageName segment, capitalized)'),
|
|
82
|
+
templateType: z.enum(['showcase', 'blank']).optional()
|
|
83
|
+
.describe('showcase (default): the sample games with every flag on; blank: an empty game with the flags and features you pick'),
|
|
84
|
+
render: z.enum(['2d', '3d']).optional().describe('Render setup (default 2d)'),
|
|
85
|
+
flags: z.array(z.string()).optional()
|
|
86
|
+
.describe('blank only: flags from game_describe (required flags are added); showcase has every flag on'),
|
|
87
|
+
features: z.array(z.string()).optional().describe('blank only: extra feature modules (lower case names)'),
|
|
88
|
+
minSdk: z.union([z.string(), z.number()]).optional().describe('Android min SDK, e.g. "25" or 25'),
|
|
89
|
+
iosVersion: z.union([z.string(), z.number()]).optional().describe('iOS deployment target, e.g. "15.0"'),
|
|
90
|
+
ci: z.boolean().optional(),
|
|
91
|
+
ids: z.record(z.string(), z.record(z.string(), z.string())).optional()
|
|
92
|
+
.describe('Real IDs by group (admob, revenueCat, gameServices; fields in game_describe), each needing its flag. ' +
|
|
93
|
+
'A group left out keeps the test IDs / placeholders, reported as a warning'),
|
|
94
|
+
templateRef: z.string().optional().describe('Override the pinned game template tag')
|
|
95
|
+
}
|
|
96
|
+
}, async (args, extra) => runGameGenerate(args, {
|
|
97
|
+
cacheDir, remoteUrl, onProgress: progressReporter(extra, 2), generatorVersion: `mcp@${pkg.version}`
|
|
98
|
+
}));
|
|
99
|
+
|
|
56
100
|
return server;
|
|
57
101
|
}
|
|
58
102
|
|