packkit-mcp 0.1.3 → 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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/package.json +2 -2
  3. package/server.js +49 -22
package/README.md CHANGED
@@ -25,6 +25,12 @@ Add to your MCP client config (e.g. Claude Desktop's `claude_desktop_config.json
25
25
 
26
26
  Then ask your agent things like *"scaffold a React component library called ui with Storybook"* or *"preview a Hono service named api"*.
27
27
 
28
+ Also listed on the [official MCP registry](https://registry.modelcontextprotocol.io) as `io.github.DanMat/packkit-mcp` and on [Glama](https://glama.ai/mcp/servers/DanMat/create-packkit).
29
+
30
+ ## Releasing
31
+
32
+ Publishing a new version touches npm, the official MCP registry and Glama — see **[RELEASING.md](RELEASING.md)**.
33
+
28
34
  ## License
29
35
 
30
36
  [MIT](https://github.com/DanMat/create-packkit/blob/main/LICENSE) © DanMat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "packkit-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.DanMat/packkit-mcp",
5
5
  "description": "MCP server for Packkit — let AI agents scaffold modern npm packages, CLIs, services, and apps as a native tool.",
6
6
  "type": "module",
@@ -34,6 +34,6 @@
34
34
  "homepage": "https://danmat.github.io/create-packkit/",
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
- "create-packkit": "^2.7.0"
37
+ "create-packkit": "^2.8.0"
38
38
  }
39
39
  }
package/server.js CHANGED
@@ -2,10 +2,17 @@
2
2
  // Packkit MCP server — exposes Packkit scaffolding as Model Context Protocol
3
3
  // tools so agents (Claude Desktop, Cursor, etc.) can generate projects natively.
4
4
 
5
- import { mkdir, writeFile } from 'node:fs/promises';
6
- import { existsSync, readdirSync } from 'node:fs';
7
- import { dirname, join, resolve } from 'node:path';
8
- import { spawnSync } from 'node:child_process';
5
+ import { join, resolve } from 'node:path';
6
+ import {
7
+ writeProject,
8
+ existingEntries,
9
+ gitInit,
10
+ installDeps,
11
+ hasCommand,
12
+ githubLogin,
13
+ createGithubRepo,
14
+ commitAll,
15
+ } from 'create-packkit/scaffold';
9
16
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
17
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
18
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -57,6 +64,9 @@ const TOOLS = [
57
64
  directory: { type: 'string', description: 'Parent directory to create the project in (default: current working directory)' },
58
65
  install: { type: 'boolean', description: 'Install dependencies (default false)' },
59
66
  git: { type: 'boolean', description: 'git init + initial commit (default false)' },
67
+ merge: { type: 'boolean', description: 'Scaffold into a non-empty directory. Existing files are never overwritten — colliding ones are skipped and reported. Use this when the target is an already-cloned repo.' },
68
+ github: { type: 'boolean', description: 'Create the repository on GitHub and push to it, using the `gh` CLI. Requires git. Private unless "public" is set.' },
69
+ public: { type: 'boolean', description: 'When creating the repository, make it public (default: private)' },
60
70
  },
61
71
  required: ['name'],
62
72
  },
@@ -66,19 +76,11 @@ const TOOLS = [
66
76
  const text = (t) => ({ content: [{ type: 'text', text: t }] });
67
77
  const fail = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
68
78
 
69
- async function writeProject(targetDir, files) {
70
- for (const [rel, contents] of Object.entries(files)) {
71
- const full = join(targetDir, rel);
72
- await mkdir(dirname(full), { recursive: true });
73
- await writeFile(full, contents);
74
- }
75
- }
76
-
77
79
  function fileTree(files) {
78
80
  return Object.keys(files).sort().map((p) => ` ${p}`).join('\n');
79
81
  }
80
82
 
81
- const server = new Server({ name: 'packkit', version: '0.1.3' }, { capabilities: { tools: {} } });
83
+ const server = new Server({ name: 'packkit', version: '0.2.0' }, { capabilities: { tools: {} } });
82
84
 
83
85
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
84
86
 
@@ -99,25 +101,50 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
99
101
  if (!config.name) return fail('A "name" is required.');
100
102
  const parent = args.directory ? resolve(args.directory) : process.cwd();
101
103
  const targetDir = join(parent, config.name);
102
- if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
103
- return fail(`Target directory "${targetDir}" is not empty.`);
104
+ const occupied = existingEntries(targetDir);
105
+ if (occupied.length && !args.merge) {
106
+ return fail(
107
+ `Target directory "${targetDir}" is not empty (${occupied.slice(0, 4).join(', ')}). ` +
108
+ 'Pass merge: true to scaffold around the existing files — they are never overwritten.',
109
+ );
104
110
  }
111
+
112
+ // Creating the repo has to be settled before generating: the repository
113
+ // URL is baked into package.json links and README badges.
114
+ let slug = null;
115
+ if (args.github) {
116
+ if (!args.git) return fail('github: true also needs git: true — there must be a commit to push.');
117
+ if (!hasCommand('gh')) return fail('github: true needs the GitHub CLI (https://cli.github.com).');
118
+ const login = githubLogin();
119
+ if (!login) return fail('github: true needs an authenticated GitHub CLI. Run: gh auth login');
120
+ slug = `${login}/${config.name}`;
121
+ config.repo = `https://github.com/${slug}`;
122
+ }
123
+
105
124
  const { files, summary } = generate(config);
106
- await writeProject(targetDir, files);
125
+ const { written, skipped } = await writeProject(targetDir, files, { merge: !!args.merge });
107
126
 
108
127
  const steps = [];
128
+ if (skipped.length) steps.push(`kept ${skipped.length} existing file(s): ${skipped.join(', ')}`);
109
129
  if (args.git) {
110
- spawnSync('git', ['init', '--quiet'], { cwd: targetDir });
111
- spawnSync('git', ['add', '-A'], { cwd: targetDir });
112
- spawnSync('git', ['commit', '-m', 'Initial commit from Packkit', '--quiet'], { cwd: targetDir });
130
+ gitInit(targetDir);
113
131
  steps.push('git initialized');
114
132
  }
115
133
  if (args.install) {
116
- const ok = spawnSync(config.packageManager, ['install'], { cwd: targetDir }).status === 0;
117
- steps.push(ok ? 'dependencies installed' : 'install failed (run it manually)');
134
+ steps.push(installDeps(config.packageManager, targetDir) ? 'dependencies installed' : 'install failed (run it manually)');
135
+ }
136
+ if (slug) {
137
+ if (args.install) commitAll(targetDir, 'Add lockfile');
138
+ const res = createGithubRepo({
139
+ slug,
140
+ description: config.description,
141
+ private: !args.public,
142
+ cwd: targetDir,
143
+ });
144
+ steps.push(res.ok ? `pushed to ${config.repo}` : `repo creation failed: ${res.error}`);
118
145
  }
119
146
  return text(
120
- `Created ${summary.name} at ${targetDir}\n${summary.fileCount} files · ${summary.stack.join(' · ')}` +
147
+ `Created ${summary.name} at ${targetDir}\n${written.length} files · ${summary.stack.join(' · ')}` +
121
148
  (steps.length ? `\n${steps.join('; ')}` : ''),
122
149
  );
123
150
  }