@livestore/cli 0.4.0 → 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 (120) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixtures/mock-config.d.ts +7 -9
  3. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -1
  4. package/dist/__tests__/fixtures/mock-config.js +6 -6
  5. package/dist/__tests__/fixtures/mock-config.js.map +1 -1
  6. package/dist/__tests__/sync-operations.test.js +20 -20
  7. package/dist/__tests__/sync-operations.test.js.map +1 -1
  8. package/dist/bin.js +6 -5
  9. package/dist/bin.js.map +1 -1
  10. package/dist/cli.d.ts +2 -23
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +1 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
  15. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
  16. package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
  17. package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
  18. package/dist/commands/import-export.d.ts +5 -19
  19. package/dist/commands/import-export.d.ts.map +1 -1
  20. package/dist/commands/import-export.js +12 -12
  21. package/dist/commands/import-export.js.map +1 -1
  22. package/dist/commands/mcp-coach.d.ts +8 -7
  23. package/dist/commands/mcp-coach.d.ts.map +1 -1
  24. package/dist/commands/mcp-coach.js +10 -10
  25. package/dist/commands/mcp-coach.js.map +1 -1
  26. package/dist/commands/mcp-protocols.d.ts +7 -0
  27. package/dist/commands/mcp-protocols.d.ts.map +1 -0
  28. package/dist/commands/mcp-protocols.js +12 -0
  29. package/dist/commands/mcp-protocols.js.map +1 -0
  30. package/dist/commands/mcp-protocols.test.d.ts +2 -0
  31. package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
  32. package/dist/commands/mcp-protocols.test.js +60 -0
  33. package/dist/commands/mcp-protocols.test.js.map +1 -0
  34. package/dist/commands/mcp-stdio.test.d.ts +2 -0
  35. package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
  36. package/dist/commands/mcp-stdio.test.js +79 -0
  37. package/dist/commands/mcp-stdio.test.js.map +1 -0
  38. package/dist/commands/mcp-tool-handlers.d.ts +1 -1
  39. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
  40. package/dist/commands/mcp-tool-handlers.js +1 -0
  41. package/dist/commands/mcp-tool-handlers.js.map +1 -1
  42. package/dist/commands/mcp-tools-defs.d.ts +65 -64
  43. package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
  44. package/dist/commands/mcp-tools-defs.js +59 -52
  45. package/dist/commands/mcp-tools-defs.js.map +1 -1
  46. package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
  47. package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
  48. package/dist/commands/mcp-tools-defs.test.js +11 -0
  49. package/dist/commands/mcp-tools-defs.test.js.map +1 -0
  50. package/dist/commands/mcp.d.ts +1 -3
  51. package/dist/commands/mcp.d.ts.map +1 -1
  52. package/dist/commands/mcp.js +5 -4
  53. package/dist/commands/mcp.js.map +1 -1
  54. package/dist/commands/new-project.d.ts +19 -27
  55. package/dist/commands/new-project.d.ts.map +1 -1
  56. package/dist/commands/new-project.js +17 -16
  57. package/dist/commands/new-project.js.map +1 -1
  58. package/dist/mcp-content/architecture.d.ts +1 -1
  59. package/dist/mcp-content/architecture.d.ts.map +1 -1
  60. package/dist/mcp-content/architecture.js +3 -3
  61. package/dist/mcp-content/features.d.ts +1 -1
  62. package/dist/mcp-content/features.d.ts.map +1 -1
  63. package/dist/mcp-content/features.js +1 -1
  64. package/dist/mcp-content/getting-started.d.ts +1 -1
  65. package/dist/mcp-content/getting-started.d.ts.map +1 -1
  66. package/dist/mcp-content/getting-started.js +37 -37
  67. package/dist/mcp-content/overview.d.ts +1 -1
  68. package/dist/mcp-content/overview.d.ts.map +1 -1
  69. package/dist/mcp-content/overview.js +6 -6
  70. package/dist/mcp-content/schemas/blog.d.ts +1 -1
  71. package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
  72. package/dist/mcp-content/schemas/blog.js +25 -25
  73. package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
  74. package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
  75. package/dist/mcp-content/schemas/ecommerce.js +67 -67
  76. package/dist/mcp-content/schemas/social.d.ts +1 -1
  77. package/dist/mcp-content/schemas/social.d.ts.map +1 -1
  78. package/dist/mcp-content/schemas/social.js +48 -48
  79. package/dist/mcp-content/schemas/todo.d.ts +1 -1
  80. package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
  81. package/dist/mcp-content/schemas/todo.js +35 -35
  82. package/dist/mcp-runtime/runtime.d.ts +2 -2
  83. package/dist/mcp-runtime/runtime.d.ts.map +1 -1
  84. package/dist/mcp-runtime/runtime.js +4 -4
  85. package/dist/mcp-runtime/runtime.js.map +1 -1
  86. package/dist/module-loader.d.ts +2 -2
  87. package/dist/module-loader.d.ts.map +1 -1
  88. package/dist/module-loader.js +6 -6
  89. package/dist/module-loader.js.map +1 -1
  90. package/dist/sync-operations.d.ts +24 -30
  91. package/dist/sync-operations.d.ts.map +1 -1
  92. package/dist/sync-operations.js +10 -12
  93. package/dist/sync-operations.js.map +1 -1
  94. package/package.json +30 -44
  95. package/src/__tests__/fixtures/mock-config.ts +8 -8
  96. package/src/__tests__/sync-operations.test.ts +21 -35
  97. package/src/bin.ts +7 -6
  98. package/src/cli.ts +1 -1
  99. package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
  100. package/src/commands/import-export.ts +29 -29
  101. package/src/commands/mcp-coach.ts +12 -11
  102. package/src/commands/mcp-protocols.test.ts +76 -0
  103. package/src/commands/mcp-protocols.ts +12 -0
  104. package/src/commands/mcp-stdio.test.ts +93 -0
  105. package/src/commands/mcp-tool-handlers.ts +11 -8
  106. package/src/commands/mcp-tools-defs.test.ts +13 -0
  107. package/src/commands/mcp-tools-defs.ts +53 -51
  108. package/src/commands/mcp.ts +7 -4
  109. package/src/commands/new-project.ts +49 -36
  110. package/src/mcp-content/architecture.ts +3 -3
  111. package/src/mcp-content/features.ts +1 -1
  112. package/src/mcp-content/getting-started.ts +37 -37
  113. package/src/mcp-content/overview.ts +6 -6
  114. package/src/mcp-content/schemas/blog.ts +25 -25
  115. package/src/mcp-content/schemas/ecommerce.ts +67 -67
  116. package/src/mcp-content/schemas/social.ts +48 -48
  117. package/src/mcp-content/schemas/todo.ts +35 -35
  118. package/src/mcp-runtime/runtime.ts +7 -7
  119. package/src/module-loader.ts +10 -10
  120. package/src/sync-operations.ts +16 -16
@@ -6,8 +6,8 @@ import { Cli } from '@livestore/utils/node'
6
6
 
7
7
  import * as SyncOps from '../sync-operations.ts'
8
8
 
9
- const jsonStringifyPretty = Schema.encodeSync(Schema.parseJson({ space: 2 }))
10
- const jsonParse = Schema.decodeUnknownSync(Schema.parseJson())
9
+ const jsonStringifyPretty = (value: SyncOps.ExportFile) => JSON.stringify(value, null, 2)
10
+ const jsonParse = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))
11
11
 
12
12
  const LARGE_EVENT_WARNING_THRESHOLD = 100_000
13
13
 
@@ -171,19 +171,19 @@ const importEvents = ({
171
171
  export const exportCommand = Cli.Command.make(
172
172
  'export',
173
173
  {
174
- config: Cli.Options.text('config').pipe(
175
- Cli.Options.withAlias('c'),
176
- Cli.Options.withDescription('Path to the config module that exports schema and syncBackend'),
174
+ config: Cli.Flag.string('config').pipe(
175
+ Cli.Flag.withAlias('c'),
176
+ Cli.Flag.withDescription('Path to the config module that exports schema and syncBackend'),
177
177
  ),
178
- storeId: Cli.Options.text('store-id').pipe(
179
- Cli.Options.withAlias('i'),
180
- Cli.Options.withDescription('Store identifier'),
178
+ storeId: Cli.Flag.string('store-id').pipe(
179
+ Cli.Flag.withAlias('i'),
180
+ Cli.Flag.withDescription('Store identifier'),
181
181
  ),
182
- clientId: Cli.Options.text('client-id').pipe(
183
- Cli.Options.withDefault('cli-export'),
184
- Cli.Options.withDescription('Client identifier for the sync connection'),
182
+ clientId: Cli.Flag.string('client-id').pipe(
183
+ Cli.Flag.withDefault('cli-export'),
184
+ Cli.Flag.withDescription('Client identifier for the sync connection'),
185
185
  ),
186
- output: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Output JSON file path')),
186
+ output: Cli.Argument.string('file').pipe(Cli.Argument.withDescription('Output JSON file path')),
187
187
  },
188
188
  Effect.fn(function* ({
189
189
  config,
@@ -218,28 +218,28 @@ export const exportCommand = Cli.Command.make(
218
218
  export const importCommand = Cli.Command.make(
219
219
  'import',
220
220
  {
221
- config: Cli.Options.text('config').pipe(
222
- Cli.Options.withAlias('c'),
223
- Cli.Options.withDescription('Path to the config module that exports schema and syncBackend'),
221
+ config: Cli.Flag.string('config').pipe(
222
+ Cli.Flag.withAlias('c'),
223
+ Cli.Flag.withDescription('Path to the config module that exports schema and syncBackend'),
224
224
  ),
225
- storeId: Cli.Options.text('store-id').pipe(
226
- Cli.Options.withAlias('i'),
227
- Cli.Options.withDescription('Store identifier'),
225
+ storeId: Cli.Flag.string('store-id').pipe(
226
+ Cli.Flag.withAlias('i'),
227
+ Cli.Flag.withDescription('Store identifier'),
228
228
  ),
229
- clientId: Cli.Options.text('client-id').pipe(
230
- Cli.Options.withDefault('cli-import'),
231
- Cli.Options.withDescription('Client identifier for the sync connection'),
229
+ clientId: Cli.Flag.string('client-id').pipe(
230
+ Cli.Flag.withDefault('cli-import'),
231
+ Cli.Flag.withDescription('Client identifier for the sync connection'),
232
232
  ),
233
- force: Cli.Options.boolean('force').pipe(
234
- Cli.Options.withAlias('f'),
235
- Cli.Options.withDefault(false),
236
- Cli.Options.withDescription('Force import even if store ID does not match'),
233
+ force: Cli.Flag.boolean('force').pipe(
234
+ Cli.Flag.withAlias('f'),
235
+ Cli.Flag.withDefault(false),
236
+ Cli.Flag.withDescription('Force import even if store ID does not match'),
237
237
  ),
238
- dryRun: Cli.Options.boolean('dry-run').pipe(
239
- Cli.Options.withDefault(false),
240
- Cli.Options.withDescription('Validate the import file without actually importing'),
238
+ dryRun: Cli.Flag.boolean('dry-run').pipe(
239
+ Cli.Flag.withDefault(false),
240
+ Cli.Flag.withDescription('Validate the import file without actually importing'),
241
241
  ),
242
- input: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Input JSON file to import')),
242
+ input: Cli.Argument.string('file').pipe(Cli.Argument.withDescription('Input JSON file to import')),
243
243
  },
244
244
  Effect.fn(function* ({
245
245
  config,
@@ -1,36 +1,36 @@
1
- import { LanguageModel } from '@effect/ai'
1
+ import { AiError, LanguageModel, Prompt, Tool } from 'effect/unstable/ai'
2
2
  import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'
3
3
 
4
- import { AiError, Config, Effect, FetchHttpClient, Layer, Prompt, Schema, Tool } from '@livestore/utils/effect'
4
+ import { Config, Effect, FetchHttpClient, Layer, Schema } from '@livestore/utils/effect'
5
5
 
6
6
  // Define the coach tool that analyzes LiveStore usage
7
7
  export const coachTool = Tool.make('livestore_coach', {
8
8
  description:
9
9
  'Analyze LiveStore code (schemas, queries, mutations, etc.) and provide AI-powered feedback on best practices, performance, and improvements.',
10
- parameters: {
11
- code: Schema.String.annotations({
10
+ parameters: Schema.Struct({
11
+ code: Schema.String.annotate({
12
12
  description: 'The LiveStore code to analyze (TypeScript/JavaScript)',
13
13
  }),
14
14
  codeType: Schema.optional(
15
- Schema.String.annotations({
15
+ Schema.String.annotate({
16
16
  description: "Type of code being analyzed: 'schema', 'query', 'mutation', 'component', or 'general'",
17
17
  }),
18
18
  ),
19
- },
19
+ }),
20
20
  success: Schema.Struct({
21
- feedback: Schema.String.annotations({
21
+ feedback: Schema.String.annotate({
22
22
  description: 'AI-generated feedback and recommendations for the code',
23
23
  }),
24
24
  score: Schema.optional(
25
- Schema.Number.annotations({
25
+ Schema.Number.annotate({
26
26
  description: 'Code quality score from 1-10 (optional)',
27
27
  }),
28
28
  ),
29
29
  suggestions: Schema.Array(
30
- Schema.String.annotations({
30
+ Schema.String.annotate({
31
31
  description: 'Specific actionable suggestions for improvement',
32
32
  }),
33
- ).annotations({
33
+ ).annotate({
34
34
  description: 'List of specific improvement suggestions',
35
35
  }),
36
36
  }),
@@ -58,7 +58,8 @@ type CoachToolResult = {
58
58
  export const coachToolHandler: (input: CoachToolHandlerInput) => Effect.Effect<CoachToolResult, AiError.AiError> =
59
59
  Effect.fn('mcp-coach-handler')(({ code, codeType }) => {
60
60
  const effect = Effect.gen(function* () {
61
- const codeTypeContext = codeType !== undefined ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
61
+ const codeTypeContext =
62
+ codeType !== undefined ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
62
63
 
63
64
  const prompt = Prompt.makeMessage('user', {
64
65
  content: [
@@ -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,4 +1,6 @@
1
- import { Effect, FetchHttpClient, Layer, type Toolkit } from '@livestore/utils/effect'
1
+ import { type Toolkit } from 'effect/unstable/ai'
2
+
3
+ import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
2
4
  import { PlatformNode } from '@livestore/utils/node'
3
5
 
4
6
  import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
@@ -59,13 +61,14 @@ export const schema = Schema.create({
59
61
  }
60
62
 
61
63
  // Generate a basic custom schema based on description
62
- const tableName = customDescription.toLowerCase().includes('user') === true
63
- ? 'users'
64
- : customDescription.toLowerCase().includes('product') === true
65
- ? 'products'
66
- : customDescription.toLowerCase().includes('post') === true
67
- ? 'posts'
68
- : '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'
69
72
 
70
73
  schemaCode = `// Custom schema based on: ${customDescription}
71
74
  import { Schema } from '@livestore/livestore'
@@ -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
+ })
@@ -1,4 +1,6 @@
1
- import { Schema, Tool, Toolkit } from '@livestore/utils/effect'
1
+ import { Tool, Toolkit } from 'effect/unstable/ai'
2
+
3
+ import { Schema } from '@livestore/utils/effect'
2
4
 
3
5
  import { coachTool } from './mcp-coach.ts'
4
6
 
@@ -8,32 +10,32 @@ export const livestoreToolkit = Toolkit.make(
8
10
  Tool.make('livestore_generate_schema', {
9
11
  description:
10
12
  '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.',
11
- parameters: {
12
- schemaType: Schema.String.annotations({
13
+ parameters: Schema.Struct({
14
+ schemaType: Schema.String.annotate({
13
15
  description: "Schema type: 'todo', 'blog', 'social', 'ecommerce', or 'custom'",
14
16
  }),
15
17
  customDescription: Schema.optional(
16
- Schema.String.annotations({
18
+ Schema.String.annotate({
17
19
  description:
18
20
  "For custom schemas: describe your data model needs (e.g., 'user management system with roles and permissions')",
19
21
  }),
20
22
  ),
21
- },
23
+ }),
22
24
  success: Schema.Struct({
23
- schemaCode: Schema.String.annotations({ description: 'The generated LiveStore schema TypeScript code' }),
24
- explanation: Schema.String.annotations({ description: 'Brief explanation of the schema structure' }),
25
+ schemaCode: Schema.String.annotate({ description: 'The generated LiveStore schema TypeScript code' }),
26
+ explanation: Schema.String.annotate({ description: 'Brief explanation of the schema structure' }),
25
27
  }),
26
28
  }),
27
29
 
28
30
  Tool.make('livestore_get_example_schema', {
29
31
  description:
30
32
  'Get a complete example LiveStore schema with TypeScript code. Returns ready-to-use schema definitions for common application types.',
31
- parameters: {
32
- type: Schema.String.annotations({ description: "Example type: 'todo', 'blog', 'social', or 'ecommerce'" }),
33
- },
33
+ parameters: Schema.Struct({
34
+ type: Schema.String.annotate({ description: "Example type: 'todo', 'blog', 'social', or 'ecommerce'" }),
35
+ }),
34
36
  success: Schema.Struct({
35
- schemaCode: Schema.String.annotations({ description: 'The complete LiveStore schema code' }),
36
- description: Schema.String.annotations({ description: 'Description of what this schema models' }),
37
+ schemaCode: Schema.String.annotate({ description: 'The complete LiveStore schema code' }),
38
+ description: Schema.String.annotate({ description: 'Description of what this schema models' }),
37
39
  }),
38
40
  })
39
41
  .annotate(Tool.Readonly, true)
@@ -86,27 +88,27 @@ Returns on success:
86
88
  "eventNames": ["<event-name-1>", "<event-name-2>", "..."]
87
89
  }
88
90
  }`,
89
- parameters: {
90
- configPath: Schema.String.annotations({
91
+ parameters: Schema.Struct({
92
+ configPath: Schema.String.annotate({
91
93
  description: 'Path to a module that exports named variables: schema and syncBackend',
92
94
  }),
93
- storeId: Schema.String.annotations({ description: 'Required store id for the LiveStore instance.' }),
95
+ storeId: Schema.String.annotate({ description: 'Required store id for the LiveStore instance.' }),
94
96
  clientId: Schema.optional(
95
- Schema.String.annotations({ description: 'Optional client id for the LiveStore instance.' }),
97
+ Schema.String.annotate({ description: 'Optional client id for the LiveStore instance.' }),
96
98
  ),
97
99
  sessionId: Schema.optional(
98
- Schema.String.annotations({ description: 'Optional session id for the LiveStore instance.' }),
100
+ Schema.String.annotate({ description: 'Optional session id for the LiveStore instance.' }),
99
101
  ),
100
- },
102
+ }),
101
103
  success: Schema.Struct({
102
104
  storeId: Schema.String,
103
105
  clientId: Schema.String,
104
106
  sessionId: Schema.String,
105
107
  schemaInfo: Schema.Struct({
106
- tableNames: Schema.Array(Schema.String).annotations({
108
+ tableNames: Schema.Array(Schema.String).annotate({
107
109
  description: 'Non-system table names in the connected schema',
108
110
  }),
109
- eventNames: Schema.Array(Schema.String).annotations({
111
+ eventNames: Schema.Array(Schema.String).annotate({
110
112
  description: 'Canonical event names defined by the connected schema',
111
113
  }),
112
114
  }),
@@ -137,17 +139,17 @@ Returns on success:
137
139
  "rows": [{ "col": "value" }],
138
140
  "rowCount": 1
139
141
  }`,
140
- parameters: {
141
- sql: Schema.String.annotations({ description: 'The SQL query to execute' }),
142
- bindValues: Schema.Union(
143
- Schema.Array(Schema.JsonValue),
144
- Schema.Record({ key: Schema.String, value: Schema.JsonValue }),
145
- ).annotations({
142
+ parameters: Schema.Struct({
143
+ sql: Schema.String.annotate({ description: 'The SQL query to execute' }),
144
+ bindValues: Schema.Union([
145
+ Schema.Array(Schema.Json),
146
+ Schema.Record(Schema.String, Schema.Json),
147
+ ]).annotate({
146
148
  description: 'Bind values for the SQL query (array or record). Record keys must not start with $.',
147
149
  }),
148
- },
150
+ }),
149
151
  success: Schema.Struct({
150
- rows: Schema.Array(Schema.Record({ key: Schema.String, value: Schema.JsonValue })),
152
+ rows: Schema.Array(Schema.Record(Schema.String, Schema.Json)),
151
153
  rowCount: Schema.Number,
152
154
  }),
153
155
  }).annotate(Tool.Destructive, false),
@@ -176,16 +178,16 @@ Example parameters:
176
178
 
177
179
  Returns on success:
178
180
  { "committed": 1 }`,
179
- parameters: {
181
+ parameters: Schema.Struct({
180
182
  events: Schema.Array(
181
183
  Schema.Struct({
182
- name: Schema.String.annotations({ description: 'The name of the event' }),
183
- args: Schema.JsonValue.annotations({
184
+ name: Schema.String.annotate({ description: 'The name of the event' }),
185
+ args: Schema.Json.annotate({
184
186
  description: 'The arguments for the event as a non-stringified JSON value',
185
187
  }),
186
188
  }),
187
189
  ),
188
- },
190
+ }),
189
191
  success: Schema.Struct({ committed: Schema.Number }),
190
192
  }).annotate(Tool.Destructive, true),
191
193
 
@@ -205,18 +207,18 @@ Returns when not connected:
205
207
  {
206
208
  "_tag": "disconnected"
207
209
  }`,
208
- parameters: {},
209
- success: Schema.Union(
210
+ parameters: Tool.EmptyParams,
211
+ success: Schema.Union([
210
212
  Schema.TaggedStruct('connected', {
211
213
  storeId: Schema.String,
212
214
  clientId: Schema.String,
213
215
  sessionId: Schema.String,
214
- tableCounts: Schema.Record({ key: Schema.String, value: Schema.Number }).annotations({
216
+ tableCounts: Schema.Record(Schema.String, Schema.Number).annotate({
215
217
  description: 'Tables in the LiveStore instance with their row count',
216
218
  }),
217
219
  }),
218
220
  Schema.TaggedStruct('disconnected', {}),
219
- ),
221
+ ]),
220
222
  }).annotate(Tool.Readonly, true),
221
223
 
222
224
  Tool.make('livestore_instance_disconnect', {
@@ -224,7 +226,7 @@ Returns when not connected:
224
226
 
225
227
  Example success:
226
228
  { "_tag": "disconnected" }`,
227
- parameters: {},
229
+ parameters: Tool.EmptyParams,
228
230
  success: Schema.TaggedStruct('disconnected', {}),
229
231
  }),
230
232
 
@@ -253,18 +255,18 @@ Returns on success:
253
255
  "exportedAt": "2024-01-15T10:30:00.000Z",
254
256
  "data": { "version": 1, "storeId": "my-store", ... }
255
257
  }`,
256
- parameters: {
257
- configPath: Schema.String.annotations({
258
+ parameters: Schema.Struct({
259
+ configPath: Schema.String.annotate({
258
260
  description: 'Path to a module that exports schema and syncBackend',
259
261
  }),
260
- storeId: Schema.String.annotations({ description: 'Store identifier' }),
261
- clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-export)' })),
262
- },
262
+ storeId: Schema.String.annotate({ description: 'Store identifier' }),
263
+ clientId: Schema.optional(Schema.String.annotate({ description: 'Client identifier (default: mcp-export)' })),
264
+ }),
263
265
  success: Schema.Struct({
264
266
  storeId: Schema.String,
265
267
  eventCount: Schema.Number,
266
268
  exportedAt: Schema.String,
267
- data: Schema.JsonValue.annotations({ description: 'The export file data (can be saved or passed to import)' }),
269
+ data: Schema.Json.annotate({ description: 'The export file data (can be saved or passed to import)' }),
268
270
  }),
269
271
  }).annotate(Tool.Readonly, true),
270
272
 
@@ -295,20 +297,20 @@ Returns on success:
295
297
  "eventCount": 127,
296
298
  "dryRun": false
297
299
  }`,
298
- parameters: {
299
- configPath: Schema.String.annotations({
300
+ parameters: Schema.Struct({
301
+ configPath: Schema.String.annotate({
300
302
  description: 'Path to a module that exports schema and syncBackend',
301
303
  }),
302
- storeId: Schema.String.annotations({ description: 'Store identifier' }),
303
- clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-import)' })),
304
- data: Schema.JsonValue.annotations({
304
+ storeId: Schema.String.annotate({ description: 'Store identifier' }),
305
+ clientId: Schema.optional(Schema.String.annotate({ description: 'Client identifier (default: mcp-import)' })),
306
+ data: Schema.Json.annotate({
305
307
  description: 'The export data to import (from livestore_sync_export or a file)',
306
308
  }),
307
309
  force: Schema.optional(
308
- Schema.Boolean.annotations({ description: 'Force import even if store ID does not match' }),
310
+ Schema.Boolean.annotate({ description: 'Force import even if store ID does not match' }),
309
311
  ),
310
- dryRun: Schema.optional(Schema.Boolean.annotations({ description: 'Validate without actually importing' })),
311
- },
312
+ dryRun: Schema.optional(Schema.Boolean.annotate({ description: 'Validate without actually importing' })),
313
+ }),
312
314
  success: Schema.Struct({
313
315
  storeId: Schema.String,
314
316
  eventCount: Schema.Number,
@@ -1,4 +1,6 @@
1
- import { Effect, Layer, Logger, McpServer } from '@livestore/utils/effect'
1
+ import { McpServer } from 'effect/unstable/ai'
2
+
3
+ import { Effect, Layer, Logger } from '@livestore/utils/effect'
2
4
  import { Cli, PlatformNode } from '@livestore/utils/node'
3
5
 
4
6
  import { architectureContent } from '../mcp-content/architecture.ts'
@@ -11,6 +13,7 @@ import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
11
13
  import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
12
14
  import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
13
15
  import { toolHandlers } from './mcp-tool-handlers.ts'
16
+ import { supportedMcpProtocols } from './mcp-protocols.ts'
14
17
  // Tools imports
15
18
  import { livestoreToolkit } from './mcp-tools-defs.ts'
16
19
 
@@ -84,12 +87,12 @@ const mcpServerCommand = Cli.Command.make(
84
87
  return yield* McpServer.layerStdio({
85
88
  name: 'livestore-mcp',
86
89
  version: '0.1.0',
87
- stdin: PlatformNode.NodeStream.stdin,
88
- stdout: PlatformNode.NodeSink.stdout,
90
+ protocols: supportedMcpProtocols,
89
91
  }).pipe(
90
92
  Layer.provide(LivestoreResources),
91
93
  Layer.provide(LivestoreTools),
92
- Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true }))),
94
+ Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])),
95
+ Layer.provide(PlatformNode.NodeStdio.layer),
93
96
  Layer.launch,
94
97
  )
95
98
  }),