@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.
Files changed (136) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts +63 -0
  3. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/mock-config.js +94 -0
  5. package/dist/__tests__/fixtures/mock-config.js.map +1 -0
  6. package/dist/__tests__/sync-operations.test.d.ts +2 -0
  7. package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
  8. package/dist/__tests__/sync-operations.test.js +167 -0
  9. package/dist/__tests__/sync-operations.test.js.map +1 -0
  10. package/dist/bin.js +6 -5
  11. package/dist/bin.js.map +1 -1
  12. package/dist/cli.d.ts +2 -9
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +4 -5
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
  17. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
  18. package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
  19. package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
  20. package/dist/commands/import-export.d.ts +20 -0
  21. package/dist/commands/import-export.d.ts.map +1 -0
  22. package/dist/commands/import-export.js +135 -0
  23. package/dist/commands/import-export.js.map +1 -0
  24. package/dist/commands/mcp-coach.d.ts +21 -12
  25. package/dist/commands/mcp-coach.d.ts.map +1 -1
  26. package/dist/commands/mcp-coach.js +49 -53
  27. package/dist/commands/mcp-coach.js.map +1 -1
  28. package/dist/commands/mcp-protocols.d.ts +7 -0
  29. package/dist/commands/mcp-protocols.d.ts.map +1 -0
  30. package/dist/commands/mcp-protocols.js +12 -0
  31. package/dist/commands/mcp-protocols.js.map +1 -0
  32. package/dist/commands/mcp-protocols.test.d.ts +2 -0
  33. package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
  34. package/dist/commands/mcp-protocols.test.js +60 -0
  35. package/dist/commands/mcp-protocols.test.js.map +1 -0
  36. package/dist/commands/mcp-stdio.test.d.ts +2 -0
  37. package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
  38. package/dist/commands/mcp-stdio.test.js +79 -0
  39. package/dist/commands/mcp-stdio.test.js.map +1 -0
  40. package/dist/commands/mcp-tool-handlers.d.ts +6 -0
  41. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
  42. package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +80 -45
  43. package/dist/commands/mcp-tool-handlers.js.map +1 -0
  44. package/dist/commands/mcp-tools-defs.d.ts +134 -0
  45. package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
  46. package/dist/commands/mcp-tools-defs.js +285 -0
  47. package/dist/commands/mcp-tools-defs.js.map +1 -0
  48. package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
  49. package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
  50. package/dist/commands/mcp-tools-defs.test.js +11 -0
  51. package/dist/commands/mcp-tools-defs.test.js.map +1 -0
  52. package/dist/commands/mcp.d.ts +1 -3
  53. package/dist/commands/mcp.d.ts.map +1 -1
  54. package/dist/commands/mcp.js +7 -5
  55. package/dist/commands/mcp.js.map +1 -1
  56. package/dist/commands/new-project.d.ts +22 -23
  57. package/dist/commands/new-project.d.ts.map +1 -1
  58. package/dist/commands/new-project.js +85 -44
  59. package/dist/commands/new-project.js.map +1 -1
  60. package/dist/mcp-content/architecture.d.ts +1 -1
  61. package/dist/mcp-content/architecture.d.ts.map +1 -1
  62. package/dist/mcp-content/architecture.js +3 -3
  63. package/dist/mcp-content/features.d.ts +1 -1
  64. package/dist/mcp-content/features.d.ts.map +1 -1
  65. package/dist/mcp-content/features.js +1 -1
  66. package/dist/mcp-content/getting-started.d.ts +1 -1
  67. package/dist/mcp-content/getting-started.d.ts.map +1 -1
  68. package/dist/mcp-content/getting-started.js +37 -37
  69. package/dist/mcp-content/overview.d.ts +1 -1
  70. package/dist/mcp-content/overview.d.ts.map +1 -1
  71. package/dist/mcp-content/overview.js +6 -6
  72. package/dist/mcp-content/schemas/blog.d.ts +1 -1
  73. package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
  74. package/dist/mcp-content/schemas/blog.js +25 -25
  75. package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
  76. package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
  77. package/dist/mcp-content/schemas/ecommerce.js +67 -67
  78. package/dist/mcp-content/schemas/social.d.ts +1 -1
  79. package/dist/mcp-content/schemas/social.d.ts.map +1 -1
  80. package/dist/mcp-content/schemas/social.js +48 -48
  81. package/dist/mcp-content/schemas/todo.d.ts +1 -1
  82. package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
  83. package/dist/mcp-content/schemas/todo.js +35 -35
  84. package/dist/mcp-runtime/runtime.d.ts +48 -0
  85. package/dist/mcp-runtime/runtime.d.ts.map +1 -0
  86. package/dist/mcp-runtime/runtime.js +107 -0
  87. package/dist/mcp-runtime/runtime.js.map +1 -0
  88. package/dist/module-loader.d.ts +22 -0
  89. package/dist/module-loader.d.ts.map +1 -0
  90. package/dist/module-loader.js +75 -0
  91. package/dist/module-loader.js.map +1 -0
  92. package/dist/package-manager.d.ts +38 -0
  93. package/dist/package-manager.d.ts.map +1 -0
  94. package/dist/package-manager.js +33 -0
  95. package/dist/package-manager.js.map +1 -0
  96. package/dist/package-manager.test.d.ts +2 -0
  97. package/dist/package-manager.test.d.ts.map +1 -0
  98. package/dist/package-manager.test.js +57 -0
  99. package/dist/package-manager.test.js.map +1 -0
  100. package/dist/sync-operations.d.ts +115 -0
  101. package/dist/sync-operations.d.ts.map +1 -0
  102. package/dist/sync-operations.js +178 -0
  103. package/dist/sync-operations.js.map +1 -0
  104. package/package.json +57 -15
  105. package/src/__tests__/fixtures/mock-config.ts +112 -0
  106. package/src/__tests__/sync-operations.test.ts +218 -0
  107. package/src/bin.ts +8 -6
  108. package/src/cli.ts +5 -6
  109. package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
  110. package/src/commands/import-export.ts +283 -0
  111. package/src/commands/mcp-coach.ts +76 -75
  112. package/src/commands/mcp-protocols.test.ts +76 -0
  113. package/src/commands/mcp-protocols.ts +12 -0
  114. package/src/commands/mcp-stdio.test.ts +93 -0
  115. package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +100 -61
  116. package/src/commands/mcp-tools-defs.test.ts +13 -0
  117. package/src/commands/mcp-tools-defs.ts +320 -0
  118. package/src/commands/mcp.ts +10 -6
  119. package/src/commands/new-project.ts +124 -57
  120. package/src/mcp-content/architecture.ts +3 -3
  121. package/src/mcp-content/features.ts +1 -1
  122. package/src/mcp-content/getting-started.ts +37 -37
  123. package/src/mcp-content/overview.ts +6 -6
  124. package/src/mcp-content/schemas/blog.ts +25 -25
  125. package/src/mcp-content/schemas/ecommerce.ts +67 -67
  126. package/src/mcp-content/schemas/social.ts +48 -48
  127. package/src/mcp-content/schemas/todo.ts +35 -35
  128. package/src/mcp-runtime/runtime.ts +147 -0
  129. package/src/module-loader.ts +94 -0
  130. package/src/package-manager.test.ts +66 -0
  131. package/src/package-manager.ts +44 -0
  132. package/src/sync-operations.ts +360 -0
  133. package/dist/commands/mcp-tools.d.ts +0 -58
  134. package/dist/commands/mcp-tools.d.ts.map +0 -1
  135. package/dist/commands/mcp-tools.js.map +0 -1
  136. 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
- import {
3
- AiError,
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.annotations({
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.annotations({
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.annotations({
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.annotations({
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.annotations({
30
+ Schema.String.annotate({
39
31
  description: 'Specific actionable suggestions for improvement',
40
32
  }),
41
- ).annotations({
33
+ ).annotate({
42
34
  description: 'List of specific improvement suggestions',
43
35
  }),
44
36
  }),
45
37
  failure: AiError.AiError,
46
38
  })
47
39
 
48
- const OpenAiClientLayer = OpenAiClient.layerConfig({
49
- apiKey: Config.redacted('OPENAI_API_KEY'),
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
- const OpenAiModel = OpenAiLanguageModel.layer({
53
- model: 'gpt-5-nano',
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
- // Coach tool handler that uses OpenAI for analysis
57
- export const coachToolHandler = Effect.fnUntraced(
58
- function* ({ code, codeType }) {
59
- // Build the analysis prompt
60
- const codeTypeContext = codeType ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
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
- const prompt = Prompt.makeMessage('user', {
63
- content: [
64
- Prompt.makePart('text', {
65
- text: `${codeTypeContext}Please review the following code and provide helpful feedback focusing on:
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
- const systemPrompt = Prompt.makeMessage('system', {
92
- 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.`,
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
- // Get OpenAI client and call the API
96
- const llm = yield* LanguageModel.LanguageModel
97
- const completion = yield* llm.generateText({ prompt: Prompt.fromMessages([systemPrompt, prompt]) })
98
-
99
- const feedback = completion.text ?? 'Unable to generate feedback'
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 { Effect, Schema, Tool, Toolkit } from '@livestore/utils/effect'
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 { coachTool, coachToolHandler } from './mcp-coach.ts'
7
-
8
- // Create toolkit with tools and handlers following Tim Smart's pattern
9
- export const livestoreToolkit = Toolkit.make(
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
- Tool.make('livestore_get_example_schema', {
37
- description:
38
- 'Get a complete example LiveStore schema with TypeScript code. Returns ready-to-use schema definitions for common application types.',
39
- parameters: {
40
- type: Schema.String.annotations({
41
- description: "Example type: 'todo', 'blog', 'social', or 'ecommerce'",
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 (!customDescription) {
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 = customDescription.toLowerCase().includes('user')
103
- ? 'users'
104
- : customDescription.toLowerCase().includes('product')
105
- ? 'products'
106
- : customDescription.toLowerCase().includes('post')
107
- ? 'posts'
108
- : 'items'
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
+ })