summaryrefslogtreecommitdiff
path: root/mcp-servers/simple-mcp-server/.claude/commands/deploy.md
blob: 8b4049e331048ded4cc37379b43cb602cc2a0951 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# Deploy MCP Server

Deploys your MCP server to various platforms and registries.

## Usage

```
/deploy [target] [options]
```

## Targets

- `npm` - Publish to npm registry
- `docker` - Push to Docker registry
- `claude` - Register with Claude Code
- `github` - Create GitHub release

## Options

- `--tag` - Version tag (default: from package.json)
- `--registry` - Custom registry URL
- `--dry-run` - Test deployment without publishing

## Implementation

```typescript
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';

const execAsync = promisify(exec);

async function deployServer(
  target: 'npm' | 'docker' | 'claude' | 'github',
  options: {
    tag?: string;
    registry?: string;
    dryRun?: boolean;
  } = {}
) {
  console.log('๐Ÿš€ Deploying MCP Server');
  console.log('='.repeat(50));
  
  // Get version info
  const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8'));
  const version = options.tag || pkg.version;
  
  // Pre-deployment checks
  await runPreDeploymentChecks(version);
  
  // Deploy based on target
  switch (target) {
    case 'npm':
      await deployToNpm(pkg, version, options);
      break;
    case 'docker':
      await deployToDocker(pkg, version, options);
      break;
    case 'claude':
      await deployToClaude(pkg, version, options);
      break;
    case 'github':
      await deployToGitHub(pkg, version, options);
      break;
  }
  
  console.log('\nโœ… Deployment completed successfully!');
}

async function runPreDeploymentChecks(version: string) {
  console.log('\n๐Ÿ” Running pre-deployment checks...');
  
  // Check git status
  try {
    const { stdout: status } = await execAsync('git status --porcelain');
    if (status.trim()) {
      throw new Error('Working directory has uncommitted changes');
    }
    console.log('  โœ… Working directory clean');
  } catch (error) {
    if (error.message.includes('uncommitted')) {
      throw error;
    }
    console.warn('  โš ๏ธ  Git not available');
  }
  
  // Check if version tag exists
  try {
    await execAsync(`git rev-parse v${version}`);
    console.log(`  โœ… Version tag v${version} exists`);
  } catch {
    console.warn(`  โš ๏ธ  Version tag v${version} not found`);
    console.log('  Create with: git tag v' + version);
  }
  
  // Verify build exists
  const buildExists = await fs.access('dist').then(() => true).catch(() => false);
  if (!buildExists) {
    throw new Error('Build not found. Run /build first');
  }
  console.log('  โœ… Build found');
  
  // Run tests
  console.log('  Running tests...');
  try {
    await execAsync('npm test');
    console.log('  โœ… Tests passed');
  } catch {
    throw new Error('Tests must pass before deployment');
  }
}

async function deployToNpm(pkg: any, version: string, options: any) {
  console.log(`\n๐Ÿ“ฆ Deploying to npm (v${version})...`);
  
  // Check npm authentication
  try {
    await execAsync('npm whoami');
    console.log('  โœ… npm authenticated');
  } catch {
    throw new Error('Not authenticated with npm. Run: npm login');
  }
  
  // Check if version already published
  try {
    const { stdout } = await execAsync(`npm view ${pkg.name}@${version}`);
    if (stdout) {
      throw new Error(`Version ${version} already published`);
    }
  } catch (error) {
    if (error.message.includes('already published')) {
      throw error;
    }
    // Version not published yet (good)
  }
  
  // Update version if different
  if (pkg.version !== version) {
    console.log(`  Updating version to ${version}...`);
    await execAsync(`npm version ${version} --no-git-tag-version`);
  }
  
  // Publish package
  const publishCmd = options.dryRun
    ? 'npm publish --dry-run'
    : `npm publish ${options.registry ? `--registry ${options.registry}` : ''}`;
  
  console.log('  Publishing to npm...');
  const { stdout } = await execAsync(publishCmd);
  
  if (options.dryRun) {
    console.log('  ๐Ÿงช Dry run complete (not published)');
  } else {
    console.log('  โœ… Published to npm');
    console.log(`  Install with: npm install ${pkg.name}`);
    console.log(`  View at: https://www.npmjs.com/package/${pkg.name}`);
  }
}

async function deployToDocker(pkg: any, version: string, options: any) {
  console.log(`\n๐Ÿ‹ Deploying to Docker registry (v${version})...`);
  
  const imageName = pkg.name.replace('@', '').replace('/', '-');
  const registry = options.registry || 'docker.io';
  const fullImageName = `${registry}/${imageName}`;
  
  // Check Docker authentication
  try {
    await execAsync(`docker pull ${registry}/hello-world`);
    console.log('  โœ… Docker authenticated');
  } catch {
    console.warn('  โš ๏ธ  May not be authenticated with Docker registry');
  }
  
  // Build image if not exists
  try {
    await execAsync(`docker image inspect ${imageName}:${version}`);
    console.log('  โœ… Docker image exists');
  } catch {
    console.log('  Building Docker image...');
    await execAsync(`docker build -t ${imageName}:${version} -t ${imageName}:latest .`);
  }
  
  // Tag for registry
  console.log('  Tagging image...');
  await execAsync(`docker tag ${imageName}:${version} ${fullImageName}:${version}`);
  await execAsync(`docker tag ${imageName}:latest ${fullImageName}:latest`);
  
  // Push to registry
  if (!options.dryRun) {
    console.log('  Pushing to registry...');
    await execAsync(`docker push ${fullImageName}:${version}`);
    await execAsync(`docker push ${fullImageName}:latest`);
    console.log('  โœ… Pushed to Docker registry');
    console.log(`  Pull with: docker pull ${fullImageName}:${version}`);
  } else {
    console.log('  ๐Ÿงช Dry run complete (not pushed)');
  }
}

async function deployToClaude(pkg: any, version: string, options: any) {
  console.log(`\n๐Ÿค– Registering with Claude Code...`);
  
  // Create Claude integration instructions
  const instructions = `
# Claude Code Integration

## Installation

### From npm
\`\`\`bash
claude mcp add ${pkg.name} -- npx ${pkg.name}
\`\`\`

### From local build
\`\`\`bash
claude mcp add ${pkg.name} -- node ${path.resolve('dist/index.js')}
\`\`\`

### With custom configuration
\`\`\`bash
claude mcp add ${pkg.name} \\
  --env LOG_LEVEL=info \\
  --env CUSTOM_CONFIG=/path/to/config \\
  -- npx ${pkg.name}
\`\`\`

## Available Capabilities

- Tools: ${await countTools()}
- Resources: ${await countResources()}
- Prompts: ${await countPrompts()}

## Version: ${version}
`;
  
  // Save integration instructions
  const instructionsPath = 'CLAUDE_INTEGRATION.md';
  await fs.writeFile(instructionsPath, instructions);
  
  console.log('  โœ… Integration instructions created');
  console.log(`  View: ${instructionsPath}`);
  
  // Test local integration
  if (!options.dryRun) {
    console.log('  Testing local integration...');
    try {
      const { stdout } = await execAsync('claude mcp list');
      if (stdout.includes(pkg.name)) {
        console.log('  โœ… Already registered with Claude Code');
      } else {
        console.log('  Register with:');
        console.log(`    claude mcp add ${pkg.name} -- node ${path.resolve('dist/index.js')}`);
      }
    } catch {
      console.log('  Claude Code CLI not available');
    }
  }
}

async function deployToGitHub(pkg: any, version: string, options: any) {
  console.log(`\n๐Ÿ’™ Creating GitHub release (v${version})...`);
  
  // Check gh CLI
  try {
    await execAsync('gh --version');
  } catch {
    throw new Error('GitHub CLI not installed. Install from: https://cli.github.com');
  }
  
  // Check authentication
  try {
    await execAsync('gh auth status');
    console.log('  โœ… GitHub authenticated');
  } catch {
    throw new Error('Not authenticated with GitHub. Run: gh auth login');
  }
  
  // Create release notes
  const releaseNotes = await generateReleaseNotes(version);
  const notesPath = `.release-notes-${version}.md`;
  await fs.writeFile(notesPath, releaseNotes);
  
  // Create release
  if (!options.dryRun) {
    const releaseCmd = `gh release create v${version} \\
      --title "v${version}" \\
      --notes-file ${notesPath} \\
      --generate-notes`;
    
    try {
      await execAsync(releaseCmd);
      console.log('  โœ… GitHub release created');
      
      // Get release URL
      const { stdout } = await execAsync(`gh release view v${version} --json url`);
      const { url } = JSON.parse(stdout);
      console.log(`  View at: ${url}`);
    } catch (error) {
      if (error.message.includes('already exists')) {
        console.log('  โ„น๏ธ Release already exists');
      } else {
        throw error;
      }
    }
  } else {
    console.log('  ๐Ÿงช Dry run complete (release not created)');
    console.log(`  Release notes saved to: ${notesPath}`);
  }
  
  // Cleanup
  await fs.unlink(notesPath).catch(() => {});
}

async function generateReleaseNotes(version: string): Promise<string> {
  const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8'));
  
  return `
# ${pkg.name} v${version}

## ๐ŸŽ† What's New

- [Add your changes here]

## ๐Ÿ“ฆ Installation

### npm
\`\`\`bash
npm install ${pkg.name}@${version}
\`\`\`

### Claude Code
\`\`\`bash
claude mcp add ${pkg.name} -- npx ${pkg.name}@${version}
\`\`\`

## ๐Ÿ“„ Documentation

See [README.md](README.md) for usage instructions.

## ๐Ÿ”ง MCP Capabilities

- Tools: ${await countTools()}
- Resources: ${await countResources()}
- Prompts: ${await countPrompts()}
`;
}

async function countTools(): Promise<number> {
  try {
    const files = await fs.readdir('src/tools').catch(() => []);
    return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length;
  } catch {
    return 0;
  }
}

async function countResources(): Promise<number> {
  try {
    const files = await fs.readdir('src/resources').catch(() => []);
    return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length;
  } catch {
    return 0;
  }
}

async function countPrompts(): Promise<number> {
  try {
    const files = await fs.readdir('src/prompts').catch(() => []);
    return files.filter(f => f.endsWith('.ts') && f !== 'index.ts').length;
  } catch {
    return 0;
  }
}
```