@opensaas/stack-cli 0.4.0 → 0.6.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 (90) hide show
  1. package/README.md +76 -0
  2. package/dist/commands/migrate.d.ts +9 -0
  3. package/dist/commands/migrate.d.ts.map +1 -0
  4. package/dist/commands/migrate.js +299 -0
  5. package/dist/commands/migrate.js.map +1 -0
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/mcp/lib/documentation-provider.d.ts +23 -0
  9. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  10. package/dist/mcp/lib/documentation-provider.js +471 -0
  11. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  12. package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
  13. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
  14. package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
  15. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
  16. package/dist/mcp/server/index.d.ts.map +1 -1
  17. package/dist/mcp/server/index.js +103 -0
  18. package/dist/mcp/server/index.js.map +1 -1
  19. package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
  20. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  21. package/dist/mcp/server/stack-mcp-server.js +219 -0
  22. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  23. package/dist/migration/generators/migration-generator.d.ts +60 -0
  24. package/dist/migration/generators/migration-generator.d.ts.map +1 -0
  25. package/dist/migration/generators/migration-generator.js +510 -0
  26. package/dist/migration/generators/migration-generator.js.map +1 -0
  27. package/dist/migration/introspectors/index.d.ts +12 -0
  28. package/dist/migration/introspectors/index.d.ts.map +1 -0
  29. package/dist/migration/introspectors/index.js +10 -0
  30. package/dist/migration/introspectors/index.js.map +1 -0
  31. package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
  32. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
  33. package/dist/migration/introspectors/keystone-introspector.js +229 -0
  34. package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
  35. package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
  36. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
  37. package/dist/migration/introspectors/nextjs-introspector.js +159 -0
  38. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
  39. package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
  40. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
  41. package/dist/migration/introspectors/prisma-introspector.js +190 -0
  42. package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
  43. package/dist/migration/types.d.ts +86 -0
  44. package/dist/migration/types.d.ts.map +1 -0
  45. package/dist/migration/types.js +5 -0
  46. package/dist/migration/types.js.map +1 -0
  47. package/package.json +10 -2
  48. package/plugin/.claude-plugin/plugin.json +15 -0
  49. package/plugin/README.md +112 -0
  50. package/plugin/agents/migration-assistant.md +150 -0
  51. package/plugin/commands/analyze-schema.md +34 -0
  52. package/plugin/commands/generate-config.md +33 -0
  53. package/plugin/commands/validate-migration.md +34 -0
  54. package/plugin/skills/opensaas-migration/SKILL.md +192 -0
  55. package/.turbo/turbo-build.log +0 -4
  56. package/CHANGELOG.md +0 -410
  57. package/CLAUDE.md +0 -298
  58. package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
  59. package/src/commands/dev.test.ts +0 -215
  60. package/src/commands/dev.ts +0 -48
  61. package/src/commands/generate.test.ts +0 -282
  62. package/src/commands/generate.ts +0 -182
  63. package/src/commands/init.ts +0 -34
  64. package/src/commands/mcp.ts +0 -135
  65. package/src/generator/__snapshots__/context.test.ts.snap +0 -361
  66. package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
  67. package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
  68. package/src/generator/context.test.ts +0 -139
  69. package/src/generator/context.ts +0 -227
  70. package/src/generator/index.ts +0 -7
  71. package/src/generator/lists.test.ts +0 -335
  72. package/src/generator/lists.ts +0 -140
  73. package/src/generator/plugin-types.ts +0 -147
  74. package/src/generator/prisma-config.ts +0 -46
  75. package/src/generator/prisma-extensions.ts +0 -159
  76. package/src/generator/prisma.test.ts +0 -211
  77. package/src/generator/prisma.ts +0 -161
  78. package/src/generator/types.test.ts +0 -268
  79. package/src/generator/types.ts +0 -537
  80. package/src/index.ts +0 -42
  81. package/src/mcp/lib/documentation-provider.ts +0 -203
  82. package/src/mcp/lib/features/catalog.ts +0 -301
  83. package/src/mcp/lib/generators/feature-generator.ts +0 -598
  84. package/src/mcp/lib/types.ts +0 -89
  85. package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
  86. package/src/mcp/server/index.ts +0 -240
  87. package/src/mcp/server/stack-mcp-server.ts +0 -301
  88. package/tsconfig.json +0 -13
  89. package/tsconfig.tsbuildinfo +0 -1
  90. package/vitest.config.ts +0 -26
@@ -1,427 +0,0 @@
1
- /**
2
- * Wizard engine for multi-step feature implementation flows
3
- */
4
-
5
- import type { Feature, WizardSession, SessionStorage, FeatureQuestion } from '../types.js'
6
- import { getFeature } from '../features/catalog.js'
7
- import { FeatureGenerator } from '../generators/feature-generator.js'
8
-
9
- export class WizardEngine {
10
- private sessions: SessionStorage = {}
11
-
12
- /**
13
- * Start a new feature implementation wizard
14
- */
15
- async startFeature(featureId: string): Promise<{
16
- content: Array<{ type: string; text: string }>
17
- }> {
18
- const feature = getFeature(featureId)
19
- if (!feature) {
20
- return {
21
- content: [
22
- {
23
- type: 'text',
24
- text: `❌ Unknown feature: "${featureId}"\n\nAvailable features: authentication, blog, comments, file-upload, semantic-search`,
25
- },
26
- ],
27
- }
28
- }
29
-
30
- const sessionId = this.generateSessionId()
31
- const session = this.createSession(sessionId, feature)
32
- this.sessions[sessionId] = session
33
-
34
- const progressBar = this.renderProgressBar(1, feature.questions.length)
35
- const firstQuestion = this.renderQuestion(feature.questions[0], session, 1)
36
-
37
- return {
38
- content: [
39
- {
40
- type: 'text',
41
- text: `🚀 **${feature.name} Implementation**
42
-
43
- ${feature.description}
44
-
45
- **What's included**:
46
- ${feature.includes.map((item) => `- ${item}`).join('\n')}
47
-
48
- ${feature.dependsOn && feature.dependsOn.length > 0 ? `\n⚠️ **Dependencies**: This feature requires: ${feature.dependsOn.join(', ')}\n` : ''}
49
-
50
- ---
51
-
52
- ## Let's configure this feature
53
-
54
- ${firstQuestion}
55
-
56
- ---
57
-
58
- **Progress**: ${progressBar} 1/${feature.questions.length}
59
- **Session ID**: \`${sessionId}\`
60
-
61
- <details>
62
- <summary>💡 **Instructions for Claude Code**</summary>
63
-
64
- 1. Present this question to the user naturally in conversation
65
- 2. When they respond, call \`opensaas_answer_feature\` with:
66
- - sessionId: "${sessionId}"
67
- - answer: <their response>
68
- 3. Continue the conversation flow with the next question
69
- 4. Keep it natural - don't mention session IDs to the user
70
-
71
- </details>`,
72
- },
73
- ],
74
- }
75
- }
76
-
77
- /**
78
- * Process an answer and move to next question or complete the wizard
79
- */
80
- async answerQuestion(
81
- sessionId: string,
82
- answer: string | boolean | string[],
83
- ): Promise<{
84
- content: Array<{ type: string; text: string }>
85
- }> {
86
- const session = this.sessions[sessionId]
87
- if (!session) {
88
- return {
89
- content: [
90
- {
91
- type: 'text',
92
- text: `❌ Session not found: ${sessionId}\n\nPlease start a new feature implementation.`,
93
- },
94
- ],
95
- }
96
- }
97
-
98
- const currentQ = session.feature.questions[session.currentQuestionIndex]
99
-
100
- // Validate answer
101
- const validation = this.validateAnswer(answer, currentQ)
102
- if (!validation.valid) {
103
- return {
104
- content: [
105
- {
106
- type: 'text',
107
- text: `❌ **Invalid answer**: ${validation.message}\n\n${this.renderQuestion(currentQ, session, session.currentQuestionIndex + 1)}`,
108
- },
109
- ],
110
- }
111
- }
112
-
113
- // Store answer
114
- session.answers[currentQ.id] = answer
115
- session.updatedAt = new Date()
116
-
117
- // Check for follow-up questions
118
- if (currentQ.followUp) {
119
- const shouldAskFollowUp =
120
- currentQ.followUp.if === answer ||
121
- (typeof currentQ.followUp.if === 'boolean' && currentQ.followUp.if === answer)
122
-
123
- if (shouldAskFollowUp) {
124
- const followUpText = `**Follow-up**: ${currentQ.followUp.ask}`
125
- return {
126
- content: [
127
- {
128
- type: 'text',
129
- text: `✓ Recorded: ${this.formatAnswer(answer)}\n\n${followUpText}\n\n---\n\n💡 **Claude Code**: Ask this follow-up question and call \`opensaas_answer_followup\` with sessionId "${sessionId}" and their response.`,
130
- },
131
- ],
132
- }
133
- }
134
- }
135
-
136
- // Move to next question
137
- session.currentQuestionIndex++
138
-
139
- // Check if complete
140
- if (session.currentQuestionIndex >= session.feature.questions.length) {
141
- session.isComplete = true
142
- return this.generateFeatureImplementation(session)
143
- }
144
-
145
- // Render next question
146
- const nextQ = session.feature.questions[session.currentQuestionIndex]
147
- const questionNum = session.currentQuestionIndex + 1
148
- const progressBar = this.renderProgressBar(questionNum, session.feature.questions.length)
149
-
150
- return {
151
- content: [
152
- {
153
- type: 'text',
154
- text: `✓ Recorded: ${this.formatAnswer(answer)}\n\n${this.renderQuestion(nextQ, session, questionNum)}\n\n---\n\n**Progress**: ${progressBar} ${questionNum}/${session.feature.questions.length}`,
155
- },
156
- ],
157
- }
158
- }
159
-
160
- /**
161
- * Handle follow-up question answers
162
- */
163
- async answerFollowUp(
164
- sessionId: string,
165
- answer: string,
166
- ): Promise<{
167
- content: Array<{ type: string; text: string }>
168
- }> {
169
- const session = this.sessions[sessionId]
170
- if (!session) {
171
- return {
172
- content: [
173
- {
174
- type: 'text',
175
- text: `❌ Session not found: ${sessionId}`,
176
- },
177
- ],
178
- }
179
- }
180
-
181
- const currentQ = session.feature.questions[session.currentQuestionIndex]
182
- const followUpKey = `${currentQ.id}_followup`
183
-
184
- // Store follow-up answer
185
- session.followUpAnswers[followUpKey] = answer
186
- session.updatedAt = new Date()
187
-
188
- // Move to next question
189
- session.currentQuestionIndex++
190
-
191
- // Check if complete
192
- if (session.currentQuestionIndex >= session.feature.questions.length) {
193
- session.isComplete = true
194
- return this.generateFeatureImplementation(session)
195
- }
196
-
197
- // Render next question
198
- const nextQ = session.feature.questions[session.currentQuestionIndex]
199
- const questionNum = session.currentQuestionIndex + 1
200
- const progressBar = this.renderProgressBar(questionNum, session.feature.questions.length)
201
-
202
- return {
203
- content: [
204
- {
205
- type: 'text',
206
- text: `✓ Recorded: ${answer}\n\n${this.renderQuestion(nextQ, session, questionNum)}\n\n---\n\n**Progress**: ${progressBar} ${questionNum}/${session.feature.questions.length}`,
207
- },
208
- ],
209
- }
210
- }
211
-
212
- /**
213
- * Generate the complete feature implementation
214
- */
215
- private async generateFeatureImplementation(session: WizardSession): Promise<{
216
- content: Array<{ type: string; text: string }>
217
- }> {
218
- const generator = new FeatureGenerator(
219
- session.feature,
220
- session.answers,
221
- session.followUpAnswers,
222
- )
223
-
224
- const implementation = generator.generate()
225
-
226
- return {
227
- content: [
228
- {
229
- type: 'text',
230
- text: `✅ **${session.feature.name} Implementation Complete!**
231
-
232
- ---
233
-
234
- ## 📝 Update \`opensaas.config.ts\`
235
-
236
- \`\`\`typescript
237
- ${implementation.configUpdates}
238
- \`\`\`
239
-
240
- ---
241
-
242
- ## 📁 Create these files:
243
-
244
- ${implementation.files.map((file) => `### ${file.path}\n*${file.description}*\n\n\`\`\`${file.language}\n${file.content}\n\`\`\``).join('\n\n')}
245
-
246
- ---
247
-
248
- ${
249
- implementation.envVars && Object.keys(implementation.envVars).length > 0
250
- ? `## 🔐 Environment Variables\n\nAdd these to your \`.env\` file:\n\n\`\`\`bash\n${Object.entries(
251
- implementation.envVars,
252
- )
253
- .map(([key, value]) => `${key}=${value}`)
254
- .join('\n')}\n\`\`\`\n\n---\n\n`
255
- : ''
256
- }
257
-
258
- ## 🚀 Next Steps
259
-
260
- ${implementation.nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n')}
261
-
262
- ---
263
-
264
- ## 📖 Feature Documentation
265
-
266
- Add this to your project's \`DEVELOPMENT.md\`:
267
-
268
- \`\`\`markdown
269
- ${implementation.devGuideSection}
270
- \`\`\`
271
-
272
- ---
273
-
274
- 🎉 **Your ${session.feature.name} feature is ready to use!**
275
-
276
- <details>
277
- <summary>💡 **Troubleshooting**</summary>
278
-
279
- If you encounter issues:
280
- 1. Ensure all dependencies are installed: \`pnpm install\`
281
- 2. Check that environment variables are set correctly
282
- 3. Run \`pnpm generate\` to update Prisma schema
283
- 4. Run \`pnpm db:push\` to update database
284
-
285
- For more help, see the docs at https://stack.opensaas.au/
286
-
287
- </details>`,
288
- },
289
- ],
290
- }
291
- }
292
-
293
- /**
294
- * Create a new wizard session
295
- */
296
- private createSession(sessionId: string, feature: Feature): WizardSession {
297
- return {
298
- id: sessionId,
299
- featureId: feature.id,
300
- feature,
301
- currentQuestionIndex: 0,
302
- answers: {},
303
- followUpAnswers: {},
304
- isComplete: false,
305
- createdAt: new Date(),
306
- updatedAt: new Date(),
307
- }
308
- }
309
-
310
- /**
311
- * Generate a unique session ID
312
- */
313
- private generateSessionId(): string {
314
- return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
315
- }
316
-
317
- /**
318
- * Render a question for the user
319
- */
320
- private renderQuestion(
321
- question: FeatureQuestion,
322
- session: WizardSession,
323
- questionNum: number,
324
- ): string {
325
- let rendered = `**Question ${questionNum}**: ${question.text}\n\n`
326
-
327
- if (question.type === 'select' || question.type === 'multiselect') {
328
- rendered += question.options!.map((opt) => `- ${opt}`).join('\n')
329
- if (question.type === 'multiselect') {
330
- rendered += '\n\n*You can select multiple options (comma-separated)*'
331
- }
332
- } else if (question.type === 'boolean') {
333
- rendered += '*Answer: yes or no*'
334
- } else if (question.type === 'textarea') {
335
- rendered += '*Provide a detailed response*'
336
- }
337
-
338
- if (question.defaultValue !== undefined) {
339
- rendered += `\n\n*Default: ${this.formatAnswer(question.defaultValue)}*`
340
- }
341
-
342
- return rendered
343
- }
344
-
345
- /**
346
- * Validate an answer against question requirements
347
- */
348
- private validateAnswer(
349
- answer: string | boolean | string[],
350
- question: FeatureQuestion,
351
- ): { valid: boolean; message?: string } {
352
- if (question.required && !answer) {
353
- return { valid: false, message: 'This question is required' }
354
- }
355
-
356
- if (question.type === 'boolean' && typeof answer !== 'boolean') {
357
- return {
358
- valid: false,
359
- message: 'Please answer with yes/no or true/false',
360
- }
361
- }
362
-
363
- if (
364
- question.type === 'select' &&
365
- question.options &&
366
- !question.options.includes(answer as string)
367
- ) {
368
- return {
369
- valid: false,
370
- message: `Please select one of: ${question.options.join(', ')}`,
371
- }
372
- }
373
-
374
- if (question.type === 'multiselect' && question.options) {
375
- const answers = Array.isArray(answer) ? answer : [answer]
376
- const invalid = answers.filter((a) => typeof a === 'string' && !question.options!.includes(a))
377
- if (invalid.length > 0) {
378
- return {
379
- valid: false,
380
- message: `Invalid options: ${invalid.join(', ')}. Valid options: ${question.options.join(', ')}`,
381
- }
382
- }
383
- }
384
-
385
- return { valid: true }
386
- }
387
-
388
- /**
389
- * Render a progress bar
390
- */
391
- private renderProgressBar(current: number, total: number): string {
392
- const filled = Math.round((current / total) * 10)
393
- const empty = 10 - filled
394
- return '▓'.repeat(filled) + '░'.repeat(empty)
395
- }
396
-
397
- /**
398
- * Format an answer for display
399
- */
400
- private formatAnswer(answer: string | boolean | string[]): string {
401
- if (typeof answer === 'boolean') {
402
- return answer ? 'Yes' : 'No'
403
- }
404
- if (Array.isArray(answer)) {
405
- return answer.join(', ')
406
- }
407
- return answer
408
- }
409
-
410
- /**
411
- * Get session by ID (for debugging/testing)
412
- */
413
- getSession(sessionId: string): WizardSession | undefined {
414
- return this.sessions[sessionId]
415
- }
416
-
417
- /**
418
- * Clear completed sessions (cleanup)
419
- */
420
- clearCompletedSessions(): void {
421
- Object.keys(this.sessions).forEach((id) => {
422
- if (this.sessions[id].isComplete) {
423
- delete this.sessions[id]
424
- }
425
- })
426
- }
427
- }
@@ -1,240 +0,0 @@
1
- /**
2
- * OpenSaaS Stack MCP Server - Main entry point
3
- */
4
-
5
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
- import {
8
- ListToolsRequestSchema,
9
- CallToolRequestSchema,
10
- type Tool,
11
- } from '@modelcontextprotocol/sdk/types.js'
12
- import { StackMCPServer } from './stack-mcp-server.js'
13
-
14
- // Tool definitions
15
- const TOOLS: Tool[] = [
16
- {
17
- name: 'opensaas_implement_feature',
18
- description:
19
- 'Start an interactive wizard to implement a complete feature (authentication, blog, comments, file-upload, semantic-search, or custom). Returns step-by-step guidance.',
20
- inputSchema: {
21
- type: 'object',
22
- properties: {
23
- feature: {
24
- type: 'string',
25
- description:
26
- 'Feature to implement: "authentication", "blog", "comments", "file-upload", "semantic-search", or "custom"',
27
- enum: ['authentication', 'blog', 'comments', 'file-upload', 'semantic-search', 'custom'],
28
- },
29
- description: {
30
- type: 'string',
31
- description: 'Required if feature is "custom" - describe what you want to build',
32
- },
33
- },
34
- required: ['feature'],
35
- },
36
- },
37
- {
38
- name: 'opensaas_answer_feature',
39
- description:
40
- 'Answer a question in the feature implementation wizard. Use this after opensaas_implement_feature to progress through the wizard.',
41
- inputSchema: {
42
- type: 'object',
43
- properties: {
44
- sessionId: {
45
- type: 'string',
46
- description: 'Session ID from the wizard',
47
- },
48
- answer: {
49
- description: 'Your answer to the current question',
50
- oneOf: [
51
- { type: 'string' },
52
- { type: 'boolean' },
53
- { type: 'array', items: { type: 'string' } },
54
- ],
55
- },
56
- },
57
- required: ['sessionId', 'answer'],
58
- },
59
- },
60
- {
61
- name: 'opensaas_answer_followup',
62
- description: 'Answer a follow-up question in the wizard flow',
63
- inputSchema: {
64
- type: 'object',
65
- properties: {
66
- sessionId: {
67
- type: 'string',
68
- description: 'Session ID from the wizard',
69
- },
70
- answer: {
71
- type: 'string',
72
- description: 'Your answer to the follow-up question',
73
- },
74
- },
75
- required: ['sessionId', 'answer'],
76
- },
77
- },
78
- {
79
- name: 'opensaas_feature_docs',
80
- description:
81
- 'Search OpenSaaS Stack documentation for a specific topic. Returns relevant docs with code examples.',
82
- inputSchema: {
83
- type: 'object',
84
- properties: {
85
- topic: {
86
- type: 'string',
87
- description:
88
- 'Topic to search for (e.g., "access control", "field types", "hooks", "authentication")',
89
- },
90
- },
91
- required: ['topic'],
92
- },
93
- },
94
- {
95
- name: 'opensaas_list_features',
96
- description: 'List all available features that can be implemented with the wizard',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {},
100
- },
101
- },
102
- {
103
- name: 'opensaas_suggest_features',
104
- description: 'Get feature suggestions based on what features are already implemented',
105
- inputSchema: {
106
- type: 'object',
107
- properties: {
108
- currentFeatures: {
109
- type: 'array',
110
- items: { type: 'string' },
111
- description: 'List of features already implemented (e.g., ["authentication", "blog"])',
112
- },
113
- },
114
- },
115
- },
116
- {
117
- name: 'opensaas_validate_feature',
118
- description: 'Validate that a feature is properly implemented according to best practices',
119
- inputSchema: {
120
- type: 'object',
121
- properties: {
122
- feature: {
123
- type: 'string',
124
- description: 'Feature to validate (e.g., "authentication")',
125
- },
126
- configPath: {
127
- type: 'string',
128
- description: 'Path to opensaas.config.ts (optional)',
129
- },
130
- },
131
- required: ['feature'],
132
- },
133
- },
134
- ]
135
-
136
- /**
137
- * Create and start the MCP server
138
- */
139
- export async function startMCPServer() {
140
- const server = new Server(
141
- {
142
- name: 'opensaas-stack-mcp',
143
- version: '0.1.0',
144
- },
145
- {
146
- capabilities: {
147
- tools: {},
148
- },
149
- },
150
- )
151
-
152
- const stackServer = new StackMCPServer()
153
-
154
- // Register tool list handler
155
- server.setRequestHandler(ListToolsRequestSchema, async () => {
156
- return { tools: TOOLS }
157
- })
158
-
159
- // Register tool call handler
160
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
161
- const { name, arguments: args } = request.params
162
-
163
- try {
164
- switch (name) {
165
- case 'opensaas_implement_feature':
166
- return await stackServer.implementFeature(
167
- args as { feature: string; description?: string },
168
- )
169
-
170
- case 'opensaas_answer_feature':
171
- return await stackServer.answerFeatureQuestion(
172
- args as { sessionId: string; answer: string | boolean | string[] },
173
- )
174
-
175
- case 'opensaas_answer_followup':
176
- return await stackServer.answerFollowUpQuestion(
177
- args as { sessionId: string; answer: string },
178
- )
179
-
180
- case 'opensaas_feature_docs':
181
- return await stackServer.searchFeatureDocs(args as { topic: string })
182
-
183
- case 'opensaas_list_features':
184
- return await stackServer.listFeatures()
185
-
186
- case 'opensaas_suggest_features':
187
- return await stackServer.suggestFeatures(args as { currentFeatures?: string[] })
188
-
189
- case 'opensaas_validate_feature':
190
- return await stackServer.validateFeature(args as { feature: string; configPath?: string })
191
-
192
- default:
193
- return {
194
- content: [
195
- {
196
- type: 'text' as const,
197
- text: `Unknown tool: ${name}`,
198
- },
199
- ],
200
- isError: true,
201
- }
202
- }
203
- } catch (error) {
204
- const errorMessage = error instanceof Error ? error.message : String(error)
205
- console.error(`Error executing tool ${name}:`, error)
206
-
207
- return {
208
- content: [
209
- {
210
- type: 'text' as const,
211
- text: `Error: ${errorMessage}`,
212
- },
213
- ],
214
- isError: true,
215
- }
216
- }
217
- })
218
-
219
- // Periodic cleanup
220
- setInterval(
221
- () => {
222
- stackServer.cleanup()
223
- },
224
- 1000 * 60 * 15,
225
- ) // Every 15 minutes
226
-
227
- // Start server
228
- const transport = new StdioServerTransport()
229
- await server.connect(transport)
230
-
231
- console.error('OpenSaaS Stack MCP server running on stdio')
232
- }
233
-
234
- // Start if run directly
235
- if (import.meta.url === `file://${process.argv[1]}`) {
236
- startMCPServer().catch((error) => {
237
- console.error('Failed to start MCP server:', error)
238
- process.exit(1)
239
- })
240
- }