@opensaas/stack-cli 0.1.3 → 0.1.5
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +2 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +1 -6
- package/dist/commands/generate.js.map +1 -1
- package/dist/generator/index.d.ts +0 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +0 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/types.js +2 -2
- package/dist/generator/types.js.map +1 -1
- package/package.json +11 -3
- package/src/commands/generate.ts +0 -7
- package/src/generator/index.ts +0 -1
- package/src/generator/types.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/generator/mcp.d.ts +0 -14
- package/dist/generator/mcp.d.ts.map +0 -1
- package/dist/generator/mcp.js +0 -193
- package/dist/generator/mcp.js.map +0 -1
- package/src/generator/mcp.test.ts +0 -393
- package/src/generator/mcp.ts +0 -221
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { generateMcp } from './mcp.js'
|
|
3
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
4
|
-
import { text } from '@opensaas/stack-core/fields'
|
|
5
|
-
import * as fs from 'fs'
|
|
6
|
-
import * as path from 'path'
|
|
7
|
-
import * as os from 'os'
|
|
8
|
-
|
|
9
|
-
describe('MCP Generator', () => {
|
|
10
|
-
let tempDir: string
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
// Create a temporary directory for each test
|
|
14
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-'))
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
// Clean up temporary directory
|
|
19
|
-
if (fs.existsSync(tempDir)) {
|
|
20
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('generateMcp', () => {
|
|
25
|
-
it('should return false when MCP is not enabled', () => {
|
|
26
|
-
const config: OpenSaasConfig = {
|
|
27
|
-
db: {
|
|
28
|
-
provider: 'sqlite',
|
|
29
|
-
url: 'file:./dev.db',
|
|
30
|
-
},
|
|
31
|
-
lists: {
|
|
32
|
-
User: {
|
|
33
|
-
fields: {
|
|
34
|
-
name: text(),
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const generated = generateMcp(config, tempDir)
|
|
41
|
-
|
|
42
|
-
expect(generated).toBe(false)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should return true when MCP is enabled', () => {
|
|
46
|
-
const config: OpenSaasConfig = {
|
|
47
|
-
db: {
|
|
48
|
-
provider: 'sqlite',
|
|
49
|
-
url: 'file:./dev.db',
|
|
50
|
-
},
|
|
51
|
-
mcp: {
|
|
52
|
-
enabled: true,
|
|
53
|
-
},
|
|
54
|
-
lists: {
|
|
55
|
-
User: {
|
|
56
|
-
fields: {
|
|
57
|
-
name: text(),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const generated = generateMcp(config, tempDir)
|
|
64
|
-
|
|
65
|
-
expect(generated).toBe(true)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('should create MCP directory when enabled', () => {
|
|
69
|
-
const config: OpenSaasConfig = {
|
|
70
|
-
db: {
|
|
71
|
-
provider: 'sqlite',
|
|
72
|
-
url: 'file:./dev.db',
|
|
73
|
-
},
|
|
74
|
-
mcp: {
|
|
75
|
-
enabled: true,
|
|
76
|
-
},
|
|
77
|
-
lists: {},
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
generateMcp(config, tempDir)
|
|
81
|
-
|
|
82
|
-
const mcpDir = path.join(tempDir, '.opensaas', 'mcp')
|
|
83
|
-
expect(fs.existsSync(mcpDir)).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should generate tools.json with default CRUD tools', () => {
|
|
87
|
-
const config: OpenSaasConfig = {
|
|
88
|
-
db: {
|
|
89
|
-
provider: 'sqlite',
|
|
90
|
-
url: 'file:./dev.db',
|
|
91
|
-
},
|
|
92
|
-
mcp: {
|
|
93
|
-
enabled: true,
|
|
94
|
-
},
|
|
95
|
-
lists: {
|
|
96
|
-
User: {
|
|
97
|
-
fields: {
|
|
98
|
-
name: text(),
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
generateMcp(config, tempDir)
|
|
105
|
-
|
|
106
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
107
|
-
expect(fs.existsSync(toolsPath)).toBe(true)
|
|
108
|
-
|
|
109
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
110
|
-
expect(tools).toHaveLength(4) // query, create, update, delete
|
|
111
|
-
|
|
112
|
-
const toolNames = tools.map((t: { name: string }) => t.name)
|
|
113
|
-
expect(toolNames).toContain('list_user_query')
|
|
114
|
-
expect(toolNames).toContain('list_user_create')
|
|
115
|
-
expect(toolNames).toContain('list_user_update')
|
|
116
|
-
expect(toolNames).toContain('list_user_delete')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('should respect list-level MCP enabled flag', () => {
|
|
120
|
-
const config: OpenSaasConfig = {
|
|
121
|
-
db: {
|
|
122
|
-
provider: 'sqlite',
|
|
123
|
-
url: 'file:./dev.db',
|
|
124
|
-
},
|
|
125
|
-
mcp: {
|
|
126
|
-
enabled: true,
|
|
127
|
-
},
|
|
128
|
-
lists: {
|
|
129
|
-
User: {
|
|
130
|
-
fields: {
|
|
131
|
-
name: text(),
|
|
132
|
-
},
|
|
133
|
-
mcp: {
|
|
134
|
-
enabled: false,
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
Post: {
|
|
138
|
-
fields: {
|
|
139
|
-
title: text(),
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
generateMcp(config, tempDir)
|
|
146
|
-
|
|
147
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
148
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
149
|
-
|
|
150
|
-
// Only Post tools should be generated
|
|
151
|
-
const userTools = tools.filter((t: { listKey: string }) => t.listKey === 'User')
|
|
152
|
-
const postTools = tools.filter((t: { listKey: string }) => t.listKey === 'Post')
|
|
153
|
-
|
|
154
|
-
expect(userTools).toHaveLength(0)
|
|
155
|
-
expect(postTools).toHaveLength(4)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('should respect custom tool configuration', () => {
|
|
159
|
-
const config: OpenSaasConfig = {
|
|
160
|
-
db: {
|
|
161
|
-
provider: 'sqlite',
|
|
162
|
-
url: 'file:./dev.db',
|
|
163
|
-
},
|
|
164
|
-
mcp: {
|
|
165
|
-
enabled: true,
|
|
166
|
-
},
|
|
167
|
-
lists: {
|
|
168
|
-
User: {
|
|
169
|
-
fields: {
|
|
170
|
-
name: text(),
|
|
171
|
-
},
|
|
172
|
-
mcp: {
|
|
173
|
-
tools: {
|
|
174
|
-
read: true,
|
|
175
|
-
create: false,
|
|
176
|
-
update: false,
|
|
177
|
-
delete: false,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
generateMcp(config, tempDir)
|
|
185
|
-
|
|
186
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
187
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
188
|
-
|
|
189
|
-
expect(tools).toHaveLength(1)
|
|
190
|
-
expect(tools[0].name).toBe('list_user_query')
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('should include custom tools', () => {
|
|
194
|
-
const config: OpenSaasConfig = {
|
|
195
|
-
db: {
|
|
196
|
-
provider: 'sqlite',
|
|
197
|
-
url: 'file:./dev.db',
|
|
198
|
-
},
|
|
199
|
-
mcp: {
|
|
200
|
-
enabled: true,
|
|
201
|
-
},
|
|
202
|
-
lists: {
|
|
203
|
-
User: {
|
|
204
|
-
fields: {
|
|
205
|
-
name: text(),
|
|
206
|
-
},
|
|
207
|
-
mcp: {
|
|
208
|
-
tools: {
|
|
209
|
-
read: false,
|
|
210
|
-
create: false,
|
|
211
|
-
update: false,
|
|
212
|
-
delete: false,
|
|
213
|
-
},
|
|
214
|
-
customTools: [
|
|
215
|
-
{
|
|
216
|
-
name: 'user_verify_email',
|
|
217
|
-
description: 'Verify a user email address',
|
|
218
|
-
inputSchema: {},
|
|
219
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
220
|
-
handler: async () => ({}) as any,
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
generateMcp(config, tempDir)
|
|
229
|
-
|
|
230
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
231
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
232
|
-
|
|
233
|
-
expect(tools).toHaveLength(1)
|
|
234
|
-
expect(tools[0].name).toBe('user_verify_email')
|
|
235
|
-
expect(tools[0].operation).toBe('custom')
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('should generate README.md with usage instructions', () => {
|
|
239
|
-
const config: OpenSaasConfig = {
|
|
240
|
-
db: {
|
|
241
|
-
provider: 'sqlite',
|
|
242
|
-
url: 'file:./dev.db',
|
|
243
|
-
},
|
|
244
|
-
mcp: {
|
|
245
|
-
enabled: true,
|
|
246
|
-
},
|
|
247
|
-
lists: {
|
|
248
|
-
User: {
|
|
249
|
-
fields: {
|
|
250
|
-
name: text(),
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
generateMcp(config, tempDir)
|
|
257
|
-
|
|
258
|
-
const readmePath = path.join(tempDir, '.opensaas', 'mcp', 'README.md')
|
|
259
|
-
expect(fs.existsSync(readmePath)).toBe(true)
|
|
260
|
-
|
|
261
|
-
const readme = fs.readFileSync(readmePath, 'utf-8')
|
|
262
|
-
expect(readme).toContain('# MCP Tools Reference')
|
|
263
|
-
expect(readme).toContain('Available Tools')
|
|
264
|
-
expect(readme).toContain('Usage')
|
|
265
|
-
expect(readme).toContain('createMcpHandlers')
|
|
266
|
-
expect(readme).toContain('Connecting to Claude Desktop')
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('should list tools in README', () => {
|
|
270
|
-
const config: OpenSaasConfig = {
|
|
271
|
-
db: {
|
|
272
|
-
provider: 'sqlite',
|
|
273
|
-
url: 'file:./dev.db',
|
|
274
|
-
},
|
|
275
|
-
mcp: {
|
|
276
|
-
enabled: true,
|
|
277
|
-
},
|
|
278
|
-
lists: {
|
|
279
|
-
User: {
|
|
280
|
-
fields: {
|
|
281
|
-
name: text(),
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
generateMcp(config, tempDir)
|
|
288
|
-
|
|
289
|
-
const readmePath = path.join(tempDir, '.opensaas', 'mcp', 'README.md')
|
|
290
|
-
const readme = fs.readFileSync(readmePath, 'utf-8')
|
|
291
|
-
|
|
292
|
-
expect(readme).toContain('list_user_query')
|
|
293
|
-
expect(readme).toContain('list_user_create')
|
|
294
|
-
expect(readme).toContain('list_user_update')
|
|
295
|
-
expect(readme).toContain('list_user_delete')
|
|
296
|
-
expect(readme).toContain('4 tool(s) available')
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it('should handle multiple lists', () => {
|
|
300
|
-
const config: OpenSaasConfig = {
|
|
301
|
-
db: {
|
|
302
|
-
provider: 'sqlite',
|
|
303
|
-
url: 'file:./dev.db',
|
|
304
|
-
},
|
|
305
|
-
mcp: {
|
|
306
|
-
enabled: true,
|
|
307
|
-
},
|
|
308
|
-
lists: {
|
|
309
|
-
User: {
|
|
310
|
-
fields: {
|
|
311
|
-
name: text(),
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
Post: {
|
|
315
|
-
fields: {
|
|
316
|
-
title: text(),
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
generateMcp(config, tempDir)
|
|
323
|
-
|
|
324
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
325
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
326
|
-
|
|
327
|
-
expect(tools).toHaveLength(8) // 4 tools per list
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
it('should respect global defaultTools config', () => {
|
|
331
|
-
const config: OpenSaasConfig = {
|
|
332
|
-
db: {
|
|
333
|
-
provider: 'sqlite',
|
|
334
|
-
url: 'file:./dev.db',
|
|
335
|
-
},
|
|
336
|
-
mcp: {
|
|
337
|
-
enabled: true,
|
|
338
|
-
defaultTools: {
|
|
339
|
-
read: true,
|
|
340
|
-
create: true,
|
|
341
|
-
update: false,
|
|
342
|
-
delete: false,
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
lists: {
|
|
346
|
-
User: {
|
|
347
|
-
fields: {
|
|
348
|
-
name: text(),
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
generateMcp(config, tempDir)
|
|
355
|
-
|
|
356
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
357
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
358
|
-
|
|
359
|
-
expect(tools).toHaveLength(2)
|
|
360
|
-
const toolNames = tools.map((t: { name: string }) => t.name)
|
|
361
|
-
expect(toolNames).toContain('list_user_query')
|
|
362
|
-
expect(toolNames).toContain('list_user_create')
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it('should use correct dbKey for tool names', () => {
|
|
366
|
-
const config: OpenSaasConfig = {
|
|
367
|
-
db: {
|
|
368
|
-
provider: 'sqlite',
|
|
369
|
-
url: 'file:./dev.db',
|
|
370
|
-
},
|
|
371
|
-
mcp: {
|
|
372
|
-
enabled: true,
|
|
373
|
-
},
|
|
374
|
-
lists: {
|
|
375
|
-
BlogPost: {
|
|
376
|
-
fields: {
|
|
377
|
-
title: text(),
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
},
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
generateMcp(config, tempDir)
|
|
384
|
-
|
|
385
|
-
const toolsPath = path.join(tempDir, '.opensaas', 'mcp', 'tools.json')
|
|
386
|
-
const tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'))
|
|
387
|
-
|
|
388
|
-
const toolNames = tools.map((t: { name: string }) => t.name)
|
|
389
|
-
expect(toolNames).toContain('list_blogPost_query')
|
|
390
|
-
expect(toolNames).toContain('list_blogPost_create')
|
|
391
|
-
})
|
|
392
|
-
})
|
|
393
|
-
})
|
package/src/generator/mcp.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP metadata generation
|
|
3
|
-
* Generates reference files for MCP configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as fs from 'fs'
|
|
7
|
-
import * as path from 'path'
|
|
8
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
9
|
-
import { getDbKey } from '@opensaas/stack-core'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate MCP metadata if MCP is enabled
|
|
13
|
-
*/
|
|
14
|
-
export function generateMcp(config: OpenSaasConfig, outputPath: string): boolean {
|
|
15
|
-
// Skip if MCP is not enabled
|
|
16
|
-
if (!config.mcp?.enabled) {
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Ensure output directory exists
|
|
21
|
-
const mcpDir = path.join(outputPath, '.opensaas', 'mcp')
|
|
22
|
-
if (!fs.existsSync(mcpDir)) {
|
|
23
|
-
fs.mkdirSync(mcpDir, { recursive: true })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Generate tool metadata for reference
|
|
27
|
-
const tools: Array<{
|
|
28
|
-
name: string
|
|
29
|
-
description: string
|
|
30
|
-
listKey: string
|
|
31
|
-
operation: string
|
|
32
|
-
}> = []
|
|
33
|
-
|
|
34
|
-
for (const [listKey, listConfig] of Object.entries(config.lists)) {
|
|
35
|
-
if (listConfig.mcp?.enabled === false) continue
|
|
36
|
-
|
|
37
|
-
const dbKey = getDbKey(listKey)
|
|
38
|
-
const defaultTools = config.mcp?.defaultTools || {
|
|
39
|
-
read: true,
|
|
40
|
-
create: true,
|
|
41
|
-
update: true,
|
|
42
|
-
delete: true,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const enabledTools = {
|
|
46
|
-
read: listConfig.mcp?.tools?.read ?? defaultTools.read ?? true,
|
|
47
|
-
create: listConfig.mcp?.tools?.create ?? defaultTools.create ?? true,
|
|
48
|
-
update: listConfig.mcp?.tools?.update ?? defaultTools.update ?? true,
|
|
49
|
-
delete: listConfig.mcp?.tools?.delete ?? defaultTools.delete ?? true,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (enabledTools.read) {
|
|
53
|
-
tools.push({
|
|
54
|
-
name: `list_${dbKey}_query`,
|
|
55
|
-
description: `Query ${listKey} records with optional filters`,
|
|
56
|
-
listKey,
|
|
57
|
-
operation: 'query',
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (enabledTools.create) {
|
|
62
|
-
tools.push({
|
|
63
|
-
name: `list_${dbKey}_create`,
|
|
64
|
-
description: `Create a new ${listKey} record`,
|
|
65
|
-
listKey,
|
|
66
|
-
operation: 'create',
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (enabledTools.update) {
|
|
71
|
-
tools.push({
|
|
72
|
-
name: `list_${dbKey}_update`,
|
|
73
|
-
description: `Update an existing ${listKey} record`,
|
|
74
|
-
listKey,
|
|
75
|
-
operation: 'update',
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (enabledTools.delete) {
|
|
80
|
-
tools.push({
|
|
81
|
-
name: `list_${dbKey}_delete`,
|
|
82
|
-
description: `Delete a ${listKey} record`,
|
|
83
|
-
listKey,
|
|
84
|
-
operation: 'delete',
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Custom tools
|
|
89
|
-
if (listConfig.mcp?.customTools) {
|
|
90
|
-
for (const customTool of listConfig.mcp.customTools) {
|
|
91
|
-
tools.push({
|
|
92
|
-
name: customTool.name,
|
|
93
|
-
description: customTool.description,
|
|
94
|
-
listKey,
|
|
95
|
-
operation: 'custom',
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Write tools.json for reference
|
|
102
|
-
const toolsPath = path.join(mcpDir, 'tools.json')
|
|
103
|
-
fs.writeFileSync(toolsPath, JSON.stringify(tools, null, 2), 'utf-8')
|
|
104
|
-
|
|
105
|
-
// Write README with usage instructions
|
|
106
|
-
const readmePath = path.join(mcpDir, 'README.md')
|
|
107
|
-
fs.writeFileSync(
|
|
108
|
-
readmePath,
|
|
109
|
-
`# MCP Tools Reference
|
|
110
|
-
|
|
111
|
-
This directory contains metadata about your MCP configuration.
|
|
112
|
-
|
|
113
|
-
## Available Tools
|
|
114
|
-
|
|
115
|
-
${tools.length} tool(s) available:
|
|
116
|
-
|
|
117
|
-
${tools
|
|
118
|
-
.map(
|
|
119
|
-
(tool) => `- **${tool.name}** (${tool.operation}): ${tool.description}
|
|
120
|
-
- List: ${tool.listKey}`,
|
|
121
|
-
)
|
|
122
|
-
.join('\n\n')}
|
|
123
|
-
|
|
124
|
-
## Usage
|
|
125
|
-
|
|
126
|
-
Create your MCP route handler:
|
|
127
|
-
|
|
128
|
-
\`\`\`typescript
|
|
129
|
-
// app/api/mcp/[[...transport]]/route.ts
|
|
130
|
-
import { createMcpHandlers } from '@opensaas/stack-mcp'
|
|
131
|
-
import config from '@/opensaas.config'
|
|
132
|
-
import { auth } from '@/lib/auth'
|
|
133
|
-
import { getContext } from '@/.opensaas/context'
|
|
134
|
-
|
|
135
|
-
const { GET, POST, DELETE } = createMcpHandlers({
|
|
136
|
-
config,
|
|
137
|
-
auth,
|
|
138
|
-
getContext
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
export { GET, POST, DELETE }
|
|
142
|
-
\`\`\`
|
|
143
|
-
|
|
144
|
-
## Connecting to Claude Desktop
|
|
145
|
-
|
|
146
|
-
### Option 1: Remote MCP Server (Recommended for Production)
|
|
147
|
-
|
|
148
|
-
For production use with OAuth authentication, add your server via **Claude Desktop > Settings > Connectors**.
|
|
149
|
-
|
|
150
|
-
Claude Desktop requires:
|
|
151
|
-
1. Your server must be publicly accessible (use ngrok/cloudflare tunnel for local testing)
|
|
152
|
-
2. OAuth authorization server at \`/.well-known/oauth-authorization-server\`
|
|
153
|
-
3. Dynamic Client Registration (DCR) support - Better Auth MCP plugin provides this
|
|
154
|
-
|
|
155
|
-
**Note:** Remote MCP servers with OAuth cannot be configured via \`claude_desktop_config.json\` - they must be added through the Claude Desktop UI.
|
|
156
|
-
|
|
157
|
-
### Option 2: Local Development (No OAuth)
|
|
158
|
-
|
|
159
|
-
For local development without OAuth, you can create a proxy MCP server script:
|
|
160
|
-
|
|
161
|
-
1. Create \`mcp-server.js\` in your project root:
|
|
162
|
-
|
|
163
|
-
\`\`\`javascript
|
|
164
|
-
#!/usr/bin/env node
|
|
165
|
-
import { spawn } from 'child_process';
|
|
166
|
-
|
|
167
|
-
// Start Next.js dev server in background
|
|
168
|
-
const server = spawn('npm', ['run', 'dev'], { stdio: 'inherit' });
|
|
169
|
-
|
|
170
|
-
// Wait for server to be ready
|
|
171
|
-
setTimeout(() => {
|
|
172
|
-
console.log('MCP server ready at http://localhost:3000${config.mcp.basePath || '/api/mcp'}');
|
|
173
|
-
}, 3000);
|
|
174
|
-
|
|
175
|
-
process.on('SIGINT', () => {
|
|
176
|
-
server.kill();
|
|
177
|
-
process.exit();
|
|
178
|
-
});
|
|
179
|
-
\`\`\`
|
|
180
|
-
|
|
181
|
-
2. Add to \`claude_desktop_config.json\`:
|
|
182
|
-
|
|
183
|
-
\`\`\`json
|
|
184
|
-
{
|
|
185
|
-
"mcpServers": {
|
|
186
|
-
"my-app": {
|
|
187
|
-
"command": "node",
|
|
188
|
-
"args": ["mcp-server.js"]
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
\`\`\`
|
|
193
|
-
|
|
194
|
-
### Option 3: Using ngrok for Local OAuth Testing
|
|
195
|
-
|
|
196
|
-
1. Start your dev server: \`npm run dev\`
|
|
197
|
-
2. Expose with ngrok: \`ngrok http 3000\`
|
|
198
|
-
3. Add to Claude Desktop via **Settings > Connectors** with your ngrok URL
|
|
199
|
-
`,
|
|
200
|
-
'utf-8',
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
return true
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Write MCP metadata to disk
|
|
208
|
-
*/
|
|
209
|
-
export function writeMcp(config: OpenSaasConfig, outputPath: string) {
|
|
210
|
-
const generated = generateMcp(config, outputPath)
|
|
211
|
-
|
|
212
|
-
if (!generated) {
|
|
213
|
-
// Clean up MCP directory if MCP is disabled
|
|
214
|
-
const mcpDir = path.join(outputPath, '.opensaas', 'mcp')
|
|
215
|
-
if (fs.existsSync(mcpDir)) {
|
|
216
|
-
fs.rmSync(mcpDir, { recursive: true, force: true })
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return generated
|
|
221
|
-
}
|