@livestore/cli 0.4.0-dev.9 → 0.5.0-dev.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/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/fixtures/mock-config.d.ts +63 -0
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-config.js +94 -0
- package/dist/__tests__/fixtures/mock-config.js.map +1 -0
- package/dist/__tests__/sync-operations.test.d.ts +2 -0
- package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
- package/dist/__tests__/sync-operations.test.js +167 -0
- package/dist/__tests__/sync-operations.test.js.map +1 -0
- package/dist/bin.js +6 -5
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +2 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
- package/dist/commands/import-export.d.ts +20 -0
- package/dist/commands/import-export.d.ts.map +1 -0
- package/dist/commands/import-export.js +135 -0
- package/dist/commands/import-export.js.map +1 -0
- package/dist/commands/mcp-coach.d.ts +21 -12
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +49 -53
- package/dist/commands/mcp-coach.js.map +1 -1
- package/dist/commands/mcp-protocols.d.ts +7 -0
- package/dist/commands/mcp-protocols.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.js +12 -0
- package/dist/commands/mcp-protocols.js.map +1 -0
- package/dist/commands/mcp-protocols.test.d.ts +2 -0
- package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.test.js +60 -0
- package/dist/commands/mcp-protocols.test.js.map +1 -0
- package/dist/commands/mcp-stdio.test.d.ts +2 -0
- package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
- package/dist/commands/mcp-stdio.test.js +79 -0
- package/dist/commands/mcp-stdio.test.js.map +1 -0
- package/dist/commands/mcp-tool-handlers.d.ts +6 -0
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
- package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +80 -45
- package/dist/commands/mcp-tool-handlers.js.map +1 -0
- package/dist/commands/mcp-tools-defs.d.ts +134 -0
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.js +285 -0
- package/dist/commands/mcp-tools-defs.js.map +1 -0
- package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
- package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.test.js +11 -0
- package/dist/commands/mcp-tools-defs.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +1 -3
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +7 -5
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +22 -23
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +85 -44
- package/dist/commands/new-project.js.map +1 -1
- package/dist/mcp-content/architecture.d.ts +1 -1
- package/dist/mcp-content/architecture.d.ts.map +1 -1
- package/dist/mcp-content/architecture.js +3 -3
- package/dist/mcp-content/features.d.ts +1 -1
- package/dist/mcp-content/features.d.ts.map +1 -1
- package/dist/mcp-content/features.js +1 -1
- package/dist/mcp-content/getting-started.d.ts +1 -1
- package/dist/mcp-content/getting-started.d.ts.map +1 -1
- package/dist/mcp-content/getting-started.js +37 -37
- package/dist/mcp-content/overview.d.ts +1 -1
- package/dist/mcp-content/overview.d.ts.map +1 -1
- package/dist/mcp-content/overview.js +6 -6
- package/dist/mcp-content/schemas/blog.d.ts +1 -1
- package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
- package/dist/mcp-content/schemas/blog.js +25 -25
- package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
- package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
- package/dist/mcp-content/schemas/ecommerce.js +67 -67
- package/dist/mcp-content/schemas/social.d.ts +1 -1
- package/dist/mcp-content/schemas/social.d.ts.map +1 -1
- package/dist/mcp-content/schemas/social.js +48 -48
- package/dist/mcp-content/schemas/todo.d.ts +1 -1
- package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
- package/dist/mcp-content/schemas/todo.js +35 -35
- package/dist/mcp-runtime/runtime.d.ts +48 -0
- package/dist/mcp-runtime/runtime.d.ts.map +1 -0
- package/dist/mcp-runtime/runtime.js +107 -0
- package/dist/mcp-runtime/runtime.js.map +1 -0
- package/dist/module-loader.d.ts +22 -0
- package/dist/module-loader.d.ts.map +1 -0
- package/dist/module-loader.js +75 -0
- package/dist/module-loader.js.map +1 -0
- package/dist/package-manager.d.ts +38 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +33 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/package-manager.test.d.ts +2 -0
- package/dist/package-manager.test.d.ts.map +1 -0
- package/dist/package-manager.test.js +57 -0
- package/dist/package-manager.test.js.map +1 -0
- package/dist/sync-operations.d.ts +115 -0
- package/dist/sync-operations.d.ts.map +1 -0
- package/dist/sync-operations.js +178 -0
- package/dist/sync-operations.js.map +1 -0
- package/package.json +57 -15
- package/src/__tests__/fixtures/mock-config.ts +112 -0
- package/src/__tests__/sync-operations.test.ts +218 -0
- package/src/bin.ts +8 -6
- package/src/cli.ts +5 -6
- package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
- package/src/commands/import-export.ts +283 -0
- package/src/commands/mcp-coach.ts +76 -75
- package/src/commands/mcp-protocols.test.ts +76 -0
- package/src/commands/mcp-protocols.ts +12 -0
- package/src/commands/mcp-stdio.test.ts +93 -0
- package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +100 -61
- package/src/commands/mcp-tools-defs.test.ts +13 -0
- package/src/commands/mcp-tools-defs.ts +320 -0
- package/src/commands/mcp.ts +10 -6
- package/src/commands/new-project.ts +124 -57
- package/src/mcp-content/architecture.ts +3 -3
- package/src/mcp-content/features.ts +1 -1
- package/src/mcp-content/getting-started.ts +37 -37
- package/src/mcp-content/overview.ts +6 -6
- package/src/mcp-content/schemas/blog.ts +25 -25
- package/src/mcp-content/schemas/ecommerce.ts +67 -67
- package/src/mcp-content/schemas/social.ts +48 -48
- package/src/mcp-content/schemas/todo.ts +35 -35
- package/src/mcp-runtime/runtime.ts +147 -0
- package/src/module-loader.ts +94 -0
- package/src/package-manager.test.ts +66 -0
- package/src/package-manager.ts +44 -0
- package/src/sync-operations.ts +360 -0
- package/dist/commands/mcp-tools.d.ts +0 -58
- package/dist/commands/mcp-tools.d.ts.map +0 -1
- package/dist/commands/mcp-tools.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,68 +1,70 @@
|
|
|
1
|
+
import { AiError, LanguageModel, Prompt, Tool } from 'effect/unstable/ai'
|
|
1
2
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Config,
|
|
5
|
-
Effect,
|
|
6
|
-
FetchHttpClient,
|
|
7
|
-
LanguageModel,
|
|
8
|
-
Layer,
|
|
9
|
-
Prompt,
|
|
10
|
-
Schema,
|
|
11
|
-
Tool,
|
|
12
|
-
} from '@livestore/utils/effect'
|
|
3
|
+
|
|
4
|
+
import { Config, Effect, FetchHttpClient, Layer, Schema } from '@livestore/utils/effect'
|
|
13
5
|
|
|
14
6
|
// Define the coach tool that analyzes LiveStore usage
|
|
15
7
|
export const coachTool = Tool.make('livestore_coach', {
|
|
16
8
|
description:
|
|
17
9
|
'Analyze LiveStore code (schemas, queries, mutations, etc.) and provide AI-powered feedback on best practices, performance, and improvements.',
|
|
18
|
-
parameters: {
|
|
19
|
-
code: Schema.String.
|
|
10
|
+
parameters: Schema.Struct({
|
|
11
|
+
code: Schema.String.annotate({
|
|
20
12
|
description: 'The LiveStore code to analyze (TypeScript/JavaScript)',
|
|
21
13
|
}),
|
|
22
14
|
codeType: Schema.optional(
|
|
23
|
-
Schema.String.
|
|
15
|
+
Schema.String.annotate({
|
|
24
16
|
description: "Type of code being analyzed: 'schema', 'query', 'mutation', 'component', or 'general'",
|
|
25
17
|
}),
|
|
26
18
|
),
|
|
27
|
-
},
|
|
19
|
+
}),
|
|
28
20
|
success: Schema.Struct({
|
|
29
|
-
feedback: Schema.String.
|
|
21
|
+
feedback: Schema.String.annotate({
|
|
30
22
|
description: 'AI-generated feedback and recommendations for the code',
|
|
31
23
|
}),
|
|
32
24
|
score: Schema.optional(
|
|
33
|
-
Schema.Number.
|
|
25
|
+
Schema.Number.annotate({
|
|
34
26
|
description: 'Code quality score from 1-10 (optional)',
|
|
35
27
|
}),
|
|
36
28
|
),
|
|
37
29
|
suggestions: Schema.Array(
|
|
38
|
-
Schema.String.
|
|
30
|
+
Schema.String.annotate({
|
|
39
31
|
description: 'Specific actionable suggestions for improvement',
|
|
40
32
|
}),
|
|
41
|
-
).
|
|
33
|
+
).annotate({
|
|
42
34
|
description: 'List of specific improvement suggestions',
|
|
43
35
|
}),
|
|
44
36
|
}),
|
|
45
37
|
failure: AiError.AiError,
|
|
46
38
|
})
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
40
|
+
/** Composed layer: HttpClient → OpenAiClient → LanguageModel */
|
|
41
|
+
const LanguageModelLayer = OpenAiLanguageModel.layer({ model: 'gpt-4o-mini' }).pipe(
|
|
42
|
+
Layer.provide(OpenAiClient.layerConfig({ apiKey: Config.redacted('OPENAI_API_KEY') })),
|
|
43
|
+
Layer.provide(FetchHttpClient.layer),
|
|
44
|
+
)
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
type CoachToolHandlerInput = {
|
|
47
|
+
readonly code: string
|
|
48
|
+
readonly codeType?: string | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type CoachToolResult = {
|
|
52
|
+
readonly feedback: string
|
|
53
|
+
readonly score: number | undefined
|
|
54
|
+
readonly suggestions: string[]
|
|
55
|
+
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
export const coachToolHandler
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
/** Coach tool handler that uses OpenAI for analysis */
|
|
58
|
+
export const coachToolHandler: (input: CoachToolHandlerInput) => Effect.Effect<CoachToolResult, AiError.AiError> =
|
|
59
|
+
Effect.fn('mcp-coach-handler')(({ code, codeType }) => {
|
|
60
|
+
const effect = Effect.gen(function* () {
|
|
61
|
+
const codeTypeContext =
|
|
62
|
+
codeType !== undefined ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const prompt = Prompt.makeMessage('user', {
|
|
65
|
+
content: [
|
|
66
|
+
Prompt.makePart('text', {
|
|
67
|
+
text: `${codeTypeContext}Please review the following code and provide helpful feedback focusing on:
|
|
66
68
|
|
|
67
69
|
1. LiveStore best practices and conventions
|
|
68
70
|
2. Schema design and relationships (if applicable)
|
|
@@ -84,47 +86,46 @@ Please provide:
|
|
|
84
86
|
4. Any potential issues or concerns
|
|
85
87
|
|
|
86
88
|
Format your response as constructive feedback that helps developers improve their LiveStore usage.`,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
})
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const systemPrompt = Prompt.makeMessage('system', {
|
|
94
|
+
content: `You are an expert LiveStore developer and code reviewer. Provide constructive, specific, and actionable feedback on LiveStore code. Focus on best practices, performance, and maintainability.`,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const llm = yield* LanguageModel.LanguageModel
|
|
98
|
+
const completion = yield* llm.generateText({ prompt: Prompt.fromMessages([systemPrompt, prompt]) })
|
|
99
|
+
|
|
100
|
+
const feedback = completion.text ?? 'Unable to generate feedback'
|
|
101
|
+
|
|
102
|
+
const suggestions = feedback
|
|
103
|
+
.split('\n')
|
|
104
|
+
.filter(
|
|
105
|
+
(line: string) =>
|
|
106
|
+
line.trim().startsWith('-') ||
|
|
107
|
+
line.trim().startsWith('*') ||
|
|
108
|
+
line.trim().startsWith('•') ||
|
|
109
|
+
line.toLowerCase().includes('recommend') ||
|
|
110
|
+
line.toLowerCase().includes('consider') ||
|
|
111
|
+
line.toLowerCase().includes('improve'),
|
|
112
|
+
)
|
|
113
|
+
.map((line: string) => line.replace(/^[-*•]\s*/, '').trim())
|
|
114
|
+
.filter((suggestion: string) => suggestion.length > 10)
|
|
115
|
+
.slice(0, 5)
|
|
116
|
+
|
|
117
|
+
const scoreMatch = feedback.match(/(?:score|rating|quality).*?(\d+(?:\.\d+)?)/i)
|
|
118
|
+
const score = scoreMatch !== null ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
feedback,
|
|
122
|
+
score: score !== undefined && score >= 1 && score <= 10 ? score : undefined,
|
|
123
|
+
suggestions,
|
|
124
|
+
}
|
|
93
125
|
})
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Extract suggestions from the feedback (simple approach)
|
|
102
|
-
const suggestions: string[] = feedback
|
|
103
|
-
.split('\n')
|
|
104
|
-
.filter(
|
|
105
|
-
(line: string) =>
|
|
106
|
-
line.trim().startsWith('-') ||
|
|
107
|
-
line.trim().startsWith('*') ||
|
|
108
|
-
line.trim().startsWith('•') ||
|
|
109
|
-
line.toLowerCase().includes('recommend') ||
|
|
110
|
-
line.toLowerCase().includes('consider') ||
|
|
111
|
-
line.toLowerCase().includes('improve'),
|
|
112
|
-
)
|
|
113
|
-
.map((line: string) => line.replace(/^[-*•]\s*/, '').trim())
|
|
114
|
-
.filter((suggestion: string) => suggestion.length > 10)
|
|
115
|
-
.slice(0, 5) // Limit to 5 suggestions
|
|
116
|
-
|
|
117
|
-
// Try to extract a score (simple regex approach)
|
|
118
|
-
const scoreMatch = feedback.match(/(?:score|rating|quality).*?(\d+(?:\.\d+)?)/i)
|
|
119
|
-
const score = scoreMatch ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
feedback,
|
|
123
|
-
score: score && score >= 1 && score <= 10 ? score : undefined,
|
|
124
|
-
suggestions,
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
Effect.provide(Layer.provideMerge(OpenAiModel, OpenAiClientLayer)),
|
|
128
|
-
Effect.provide(FetchHttpClient.layer),
|
|
129
|
-
Effect.catchTag('ConfigError', (e) => Effect.die(e)),
|
|
130
|
-
)
|
|
127
|
+
return effect.pipe(
|
|
128
|
+
Effect.provide(LanguageModelLayer),
|
|
129
|
+
Effect.catchTag('ConfigError', (e) => Effect.die(e)),
|
|
130
|
+
)
|
|
131
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
|
|
5
|
+
import { supportedMcpProtocols } from './mcp-protocols.ts'
|
|
6
|
+
|
|
7
|
+
const protocolVersions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'] as const
|
|
8
|
+
|
|
9
|
+
Vitest.describe('supportedMcpProtocols', () => {
|
|
10
|
+
Vitest.it('keeps the fallback protocol newest-first', () => {
|
|
11
|
+
Vitest.expect(supportedMcpProtocols.map((protocol) => protocol.protocolVersion)).toEqual(protocolVersions)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
Vitest.it.each(protocolVersions)('negotiates an exact %s stdio initialize offer', async (protocolVersion) => {
|
|
15
|
+
const response = await initialize(protocolVersion)
|
|
16
|
+
|
|
17
|
+
Vitest.expect(response.result?.protocolVersion).toBe(protocolVersion)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const initialize = (protocolVersion: (typeof protocolVersions)[number]) =>
|
|
22
|
+
new Promise<InitializeResponse>((resolve, reject) => {
|
|
23
|
+
const child = spawn('bun', [new URL('./__fixtures__/mcp-protocol-server.ts', import.meta.url).pathname], {
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
+
})
|
|
26
|
+
let stdout = ''
|
|
27
|
+
let stderr = ''
|
|
28
|
+
|
|
29
|
+
const timeout = setTimeout(() => {
|
|
30
|
+
child.kill('SIGTERM')
|
|
31
|
+
reject(new Error(`MCP initialize timed out. stdout=${stdout} stderr=${stderr}`))
|
|
32
|
+
}, 5_000)
|
|
33
|
+
|
|
34
|
+
child.stdout.setEncoding('utf8')
|
|
35
|
+
child.stderr.setEncoding('utf8')
|
|
36
|
+
child.stdout.on('data', (chunk: string) => {
|
|
37
|
+
stdout += chunk
|
|
38
|
+
if (stdout.endsWith('\n') === false) return
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = JSON.parse(stdout.trim()) as InitializeResponse
|
|
42
|
+
clearTimeout(timeout)
|
|
43
|
+
child.kill('SIGTERM')
|
|
44
|
+
resolve(response)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
clearTimeout(timeout)
|
|
47
|
+
child.kill('SIGTERM')
|
|
48
|
+
reject(error)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
child.stderr.on('data', (chunk: string) => {
|
|
52
|
+
stderr += chunk
|
|
53
|
+
})
|
|
54
|
+
child.on('error', (error) => {
|
|
55
|
+
clearTimeout(timeout)
|
|
56
|
+
reject(error)
|
|
57
|
+
})
|
|
58
|
+
child.stdin.write(
|
|
59
|
+
`${JSON.stringify({
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: 1,
|
|
62
|
+
method: 'initialize',
|
|
63
|
+
params: {
|
|
64
|
+
protocolVersion,
|
|
65
|
+
capabilities: {},
|
|
66
|
+
clientInfo: { name: 'livestore-mcp-protocol-test', version: '0.0.0' },
|
|
67
|
+
},
|
|
68
|
+
})}\n`,
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
interface InitializeResponse {
|
|
73
|
+
readonly result?: {
|
|
74
|
+
readonly protocolVersion?: string
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpProtocol } from 'effect/unstable/ai'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP stdio selects an exact offered version when available and otherwise
|
|
5
|
+
* falls back to the first adapter, so keep every supported adapter newest-first.
|
|
6
|
+
*/
|
|
7
|
+
export const supportedMcpProtocols = [
|
|
8
|
+
McpProtocol.v2025_11_25,
|
|
9
|
+
McpProtocol.v2025_06_18,
|
|
10
|
+
McpProtocol.v2025_03_26,
|
|
11
|
+
McpProtocol.v2024_11_05,
|
|
12
|
+
] as const
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
|
|
5
|
+
Vitest.describe('mcp server stdio', () => {
|
|
6
|
+
Vitest.it('reserves stdout for JSON-RPC messages', async () => {
|
|
7
|
+
const response = await initialize()
|
|
8
|
+
|
|
9
|
+
Vitest.expect(response.result?.protocolVersion).toBe('2025-06-18')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/** Exercises the real CLI entrypoint so root-level warnings and logs cannot corrupt MCP framing. */
|
|
14
|
+
const initialize = () =>
|
|
15
|
+
new Promise<InitializeResponse>((resolve, reject) => {
|
|
16
|
+
const child = spawn('bun', [new URL('../bin.ts', import.meta.url).pathname, 'mcp', 'server'], {
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
})
|
|
19
|
+
let stdoutBuffer = ''
|
|
20
|
+
let stderr = ''
|
|
21
|
+
let settled = false
|
|
22
|
+
|
|
23
|
+
const finish = (result: { readonly response: InitializeResponse } | { readonly error: unknown }) => {
|
|
24
|
+
if (settled === true) return
|
|
25
|
+
settled = true
|
|
26
|
+
clearTimeout(timeout)
|
|
27
|
+
child.kill('SIGTERM')
|
|
28
|
+
if ('error' in result) reject(result.error)
|
|
29
|
+
else resolve(result.response)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
finish({ error: new Error(`MCP initialize timed out. stdout=${stdoutBuffer} stderr=${stderr}`) })
|
|
34
|
+
}, 5_000)
|
|
35
|
+
|
|
36
|
+
child.stdout.setEncoding('utf8')
|
|
37
|
+
child.stderr.setEncoding('utf8')
|
|
38
|
+
child.stdout.on('data', (chunk: string) => {
|
|
39
|
+
stdoutBuffer += chunk
|
|
40
|
+
const lines = stdoutBuffer.split('\n')
|
|
41
|
+
stdoutBuffer = lines.pop() ?? ''
|
|
42
|
+
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (line.length === 0) continue
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const message = JSON.parse(line) as InitializeResponse
|
|
48
|
+
if (message.jsonrpc !== '2.0') {
|
|
49
|
+
finish({ error: new Error(`MCP stdout contained a non-JSON-RPC message: ${line}`) })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (message.id === 1) {
|
|
53
|
+
finish({ response: message })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
finish({ error: new Error(`MCP stdout contained invalid JSON: ${line}`, { cause: error }) })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
child.stderr.on('data', (chunk: string) => {
|
|
63
|
+
stderr += chunk
|
|
64
|
+
})
|
|
65
|
+
child.on('error', (error) => {
|
|
66
|
+
finish({ error })
|
|
67
|
+
})
|
|
68
|
+
child.on('exit', (code) => {
|
|
69
|
+
if (settled === false) {
|
|
70
|
+
finish({ error: new Error(`MCP server exited before initialize completed (code ${code}). stderr=${stderr}`) })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
child.stdin.write(
|
|
74
|
+
`${JSON.stringify({
|
|
75
|
+
jsonrpc: '2.0',
|
|
76
|
+
id: 1,
|
|
77
|
+
method: 'initialize',
|
|
78
|
+
params: {
|
|
79
|
+
protocolVersion: '2025-06-18',
|
|
80
|
+
capabilities: {},
|
|
81
|
+
clientInfo: { name: 'livestore-mcp-stdio-test', version: '0.0.0' },
|
|
82
|
+
},
|
|
83
|
+
})}\n`,
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
interface InitializeResponse {
|
|
88
|
+
readonly jsonrpc?: unknown
|
|
89
|
+
readonly id?: unknown
|
|
90
|
+
readonly result?: {
|
|
91
|
+
readonly protocolVersion?: string
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,61 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Toolkit } from 'effect/unstable/ai'
|
|
2
|
+
|
|
3
|
+
import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
|
|
4
|
+
import { PlatformNode } from '@livestore/utils/node'
|
|
5
|
+
|
|
2
6
|
import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
|
|
3
7
|
import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
|
|
4
8
|
import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
|
|
5
9
|
import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
coachTool,
|
|
11
|
-
|
|
12
|
-
Tool.make('livestore_generate_schema', {
|
|
13
|
-
description:
|
|
14
|
-
'Generate a LiveStore schema for a specific use case. Choose from predefined types (todo, blog, social, ecommerce) or request a custom schema by providing a description.',
|
|
15
|
-
parameters: {
|
|
16
|
-
schemaType: Schema.String.annotations({
|
|
17
|
-
description: "Schema type: 'todo', 'blog', 'social', 'ecommerce', or 'custom'",
|
|
18
|
-
}),
|
|
19
|
-
customDescription: Schema.optional(
|
|
20
|
-
Schema.String.annotations({
|
|
21
|
-
description:
|
|
22
|
-
"For custom schemas: describe your data model needs (e.g., 'user management system with roles and permissions')",
|
|
23
|
-
}),
|
|
24
|
-
),
|
|
25
|
-
},
|
|
26
|
-
success: Schema.Struct({
|
|
27
|
-
schemaCode: Schema.String.annotations({
|
|
28
|
-
description: 'The generated LiveStore schema TypeScript code',
|
|
29
|
-
}),
|
|
30
|
-
explanation: Schema.String.annotations({
|
|
31
|
-
description: 'Brief explanation of the schema structure',
|
|
32
|
-
}),
|
|
33
|
-
}),
|
|
34
|
-
}),
|
|
10
|
+
import * as Runtime from '../mcp-runtime/runtime.ts'
|
|
11
|
+
import * as SyncOps from '../sync-operations.ts'
|
|
12
|
+
import { coachToolHandler } from './mcp-coach.ts'
|
|
13
|
+
import { livestoreToolkit } from './mcp-tools-defs.ts'
|
|
35
14
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}),
|
|
43
|
-
},
|
|
44
|
-
success: Schema.Struct({
|
|
45
|
-
schemaCode: Schema.String.annotations({
|
|
46
|
-
description: 'The complete LiveStore schema code',
|
|
47
|
-
}),
|
|
48
|
-
description: Schema.String.annotations({
|
|
49
|
-
description: 'Description of what this schema models',
|
|
50
|
-
}),
|
|
51
|
-
}),
|
|
52
|
-
})
|
|
53
|
-
.annotate(Tool.Readonly, true)
|
|
54
|
-
.annotate(Tool.Destructive, false),
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
// Tool handlers using Tim Smart's pattern
|
|
58
|
-
export const toolHandlers = livestoreToolkit.of({
|
|
15
|
+
/** Layer providing FileSystem and HttpClient for sync operations */
|
|
16
|
+
const SyncOpsLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
|
|
17
|
+
|
|
18
|
+
type LivestoreToolHandlers = Toolkit.HandlersFrom<Toolkit.Tools<typeof livestoreToolkit>>
|
|
19
|
+
|
|
20
|
+
export const toolHandlers: LivestoreToolHandlers = livestoreToolkit.of({
|
|
59
21
|
livestore_coach: coachToolHandler,
|
|
60
22
|
|
|
61
23
|
livestore_generate_schema: Effect.fnUntraced(function* ({ schemaType, customDescription }) {
|
|
@@ -80,7 +42,7 @@ export const toolHandlers = livestoreToolkit.of({
|
|
|
80
42
|
explanation = 'E-commerce schema with products, categories, orders, and inventory'
|
|
81
43
|
break
|
|
82
44
|
case 'custom': {
|
|
83
|
-
if (
|
|
45
|
+
if (customDescription == null) {
|
|
84
46
|
schemaCode = `// Custom schema requested but no description provided
|
|
85
47
|
import { Schema } from '@livestore/livestore'
|
|
86
48
|
|
|
@@ -99,13 +61,14 @@ export const schema = Schema.create({
|
|
|
99
61
|
}
|
|
100
62
|
|
|
101
63
|
// Generate a basic custom schema based on description
|
|
102
|
-
const tableName =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
64
|
+
const tableName =
|
|
65
|
+
customDescription.toLowerCase().includes('user') === true
|
|
66
|
+
? 'users'
|
|
67
|
+
: customDescription.toLowerCase().includes('product') === true
|
|
68
|
+
? 'products'
|
|
69
|
+
: customDescription.toLowerCase().includes('post') === true
|
|
70
|
+
? 'posts'
|
|
71
|
+
: 'items'
|
|
109
72
|
|
|
110
73
|
schemaCode = `// Custom schema based on: ${customDescription}
|
|
111
74
|
import { Schema } from '@livestore/livestore'
|
|
@@ -166,4 +129,80 @@ export const schema = Schema.create({
|
|
|
166
129
|
|
|
167
130
|
return { schemaCode, description }
|
|
168
131
|
}),
|
|
132
|
+
|
|
133
|
+
// Connect the single in-process LiveStore instance from user module
|
|
134
|
+
livestore_instance_connect: Effect.fnUntraced(function* ({ configPath, storeId, clientId, sessionId }) {
|
|
135
|
+
const store = yield* Runtime.init({
|
|
136
|
+
configPath,
|
|
137
|
+
storeId,
|
|
138
|
+
...(clientId !== undefined ? { clientId } : {}),
|
|
139
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
140
|
+
}).pipe(Effect.orDie)
|
|
141
|
+
const eventNames = Array.from(store.schema.eventsDefsMap.keys())
|
|
142
|
+
const tableNames = Array.from(store.schema.state.sqlite.tables.keys())
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
storeId: store.storeId,
|
|
146
|
+
clientId: store.clientId,
|
|
147
|
+
sessionId: store.sessionId,
|
|
148
|
+
schemaInfo: {
|
|
149
|
+
tableNames,
|
|
150
|
+
eventNames,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
154
|
+
|
|
155
|
+
// Execute a raw SQL query against the local client DB
|
|
156
|
+
livestore_instance_query: Effect.fnUntraced(function* ({ sql, bindValues }) {
|
|
157
|
+
return yield* Runtime.query({ sql, bindValues })
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// Validate and commit events
|
|
161
|
+
livestore_instance_commit_events: Effect.fnUntraced(function* ({ events }) {
|
|
162
|
+
return yield* Runtime.commit({ events })
|
|
163
|
+
}),
|
|
164
|
+
|
|
165
|
+
// Status
|
|
166
|
+
livestore_instance_status: Effect.fnUntraced(function* () {
|
|
167
|
+
return yield* Runtime.status
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
// Disconnect
|
|
171
|
+
livestore_instance_disconnect: Effect.fnUntraced(function* () {
|
|
172
|
+
return yield* Runtime.disconnect
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
// Sync export - pull all events from sync backend
|
|
176
|
+
livestore_sync_export: Effect.fnUntraced(function* ({ configPath, storeId, clientId }) {
|
|
177
|
+
const result = yield* SyncOps.pullEventsFromSyncBackend({
|
|
178
|
+
configPath,
|
|
179
|
+
storeId,
|
|
180
|
+
clientId: clientId ?? 'mcp-export',
|
|
181
|
+
}).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
storeId: result.storeId,
|
|
185
|
+
eventCount: result.eventCount,
|
|
186
|
+
exportedAt: result.exportedAt,
|
|
187
|
+
data: result.data,
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
// Sync import - push events to sync backend
|
|
192
|
+
livestore_sync_import: Effect.fnUntraced(function* ({ configPath, storeId, clientId, data, force, dryRun }) {
|
|
193
|
+
const result = yield* SyncOps.pushEventsToSyncBackend({
|
|
194
|
+
configPath,
|
|
195
|
+
storeId,
|
|
196
|
+
clientId: clientId ?? 'mcp-import',
|
|
197
|
+
data,
|
|
198
|
+
force: force ?? false,
|
|
199
|
+
dryRun: dryRun ?? false,
|
|
200
|
+
}).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
storeId: result.storeId,
|
|
204
|
+
eventCount: result.eventCount,
|
|
205
|
+
dryRun: result.dryRun,
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
169
208
|
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tool } from 'effect/unstable/ai'
|
|
2
|
+
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
|
|
5
|
+
import { livestoreToolkit } from './mcp-tools-defs.ts'
|
|
6
|
+
|
|
7
|
+
Vitest.describe('livestoreToolkit', () => {
|
|
8
|
+
Vitest.it('exposes MCP-compatible object parameter schemas', () => {
|
|
9
|
+
for (const tool of Object.values(livestoreToolkit.tools)) {
|
|
10
|
+
Vitest.expect(Tool.getJsonSchema(tool), tool.name).toMatchObject({ type: 'object' })
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
})
|