@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 CHANGED
@@ -1,3 +1,105 @@
1
1
  # @cmpose/mcp
2
2
 
3
- A local stdio MCP server with two tools, `cmp_describe` and `cmp_generate`, that generates Compose Multiplatform projects on your own machine through `@cmpose/core`. Add it to Claude Code with `claude mcp add cmpose -- npx -y @cmpose/mcp`. See the README at the root of the cmpose repository for details.
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.1.1",
4
- "description": "Local MCP server that generates Compose Multiplatform projects (cmpose)",
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.1.1",
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
@@ -112,4 +112,4 @@ async function runGenerate(args, { cacheDir, remoteUrl, onProgress = () => { },
112
112
  }
113
113
  }
114
114
 
115
- module.exports = { runGenerate, removeEmptyDir, NEXT_STEPS };
115
+ module.exports = { runGenerate, removeEmptyDir, targetProblem, NEXT_STEPS };
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
- const progressToken = extra && extra._meta && extra._meta.progressToken;
44
- let step = 0;
45
- const onProgress = (message) => {
46
- if (progressToken === undefined) return;
47
- step += 1;
48
- extra.sendNotification({
49
- method: 'notifications/progress',
50
- params: { progressToken, progress: step, total: 2, message }
51
- }).catch(() => { });
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