@livestore/cli 0.4.0 → 0.5.0-dev.1

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 (125) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixtures/mock-config.d.ts +8 -9
  3. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -1
  4. package/dist/__tests__/fixtures/mock-config.js +18 -8
  5. package/dist/__tests__/fixtures/mock-config.js.map +1 -1
  6. package/dist/__tests__/sync-operations.test.js +32 -22
  7. package/dist/__tests__/sync-operations.test.js.map +1 -1
  8. package/dist/bin.js +7 -6
  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 +4 -4
  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 +13 -13
  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 +11 -11
  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 +9 -8
  41. package/dist/commands/mcp-tool-handlers.js.map +1 -1
  42. package/dist/commands/mcp-tools-defs.d.ts +74 -73
  43. package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
  44. package/dist/commands/mcp-tools-defs.js +64 -57
  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 +15 -14
  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 +19 -18
  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 +5 -5
  83. package/dist/mcp-runtime/runtime.d.ts.map +1 -1
  84. package/dist/mcp-runtime/runtime.js +5 -5
  85. package/dist/mcp-runtime/runtime.js.map +1 -1
  86. package/dist/mod.js +1 -1
  87. package/dist/module-loader.d.ts +2 -2
  88. package/dist/module-loader.d.ts.map +1 -1
  89. package/dist/module-loader.js +8 -8
  90. package/dist/module-loader.js.map +1 -1
  91. package/dist/package-manager.d.ts +3 -3
  92. package/dist/package-manager.d.ts.map +1 -1
  93. package/dist/package-manager.test.js +1 -1
  94. package/dist/sync-operations.d.ts +24 -30
  95. package/dist/sync-operations.d.ts.map +1 -1
  96. package/dist/sync-operations.js +15 -15
  97. package/dist/sync-operations.js.map +1 -1
  98. package/package.json +45 -59
  99. package/src/__tests__/fixtures/.gitignore +1 -0
  100. package/src/__tests__/fixtures/mock-config.ts +21 -10
  101. package/src/__tests__/sync-operations.test.ts +36 -35
  102. package/src/bin.ts +7 -6
  103. package/src/cli.ts +1 -1
  104. package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
  105. package/src/commands/import-export.ts +29 -29
  106. package/src/commands/mcp-coach.ts +13 -12
  107. package/src/commands/mcp-protocols.test.ts +76 -0
  108. package/src/commands/mcp-protocols.ts +12 -0
  109. package/src/commands/mcp-stdio.test.ts +93 -0
  110. package/src/commands/mcp-tool-handlers.ts +11 -8
  111. package/src/commands/mcp-tools-defs.test.ts +13 -0
  112. package/src/commands/mcp-tools-defs.ts +57 -55
  113. package/src/commands/mcp.ts +9 -4
  114. package/src/commands/new-project.ts +50 -37
  115. package/src/mcp-content/architecture.ts +3 -3
  116. package/src/mcp-content/features.ts +1 -1
  117. package/src/mcp-content/getting-started.ts +37 -37
  118. package/src/mcp-content/overview.ts +6 -6
  119. package/src/mcp-content/schemas/blog.ts +25 -25
  120. package/src/mcp-content/schemas/ecommerce.ts +67 -67
  121. package/src/mcp-content/schemas/social.ts +48 -48
  122. package/src/mcp-content/schemas/todo.ts +35 -35
  123. package/src/mcp-runtime/runtime.ts +7 -7
  124. package/src/module-loader.ts +11 -12
  125. package/src/sync-operations.ts +26 -19
package/src/bin.ts CHANGED
@@ -1,25 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { liveStoreVersion } from '@livestore/common'
4
- import { Console, Effect, FetchHttpClient, Layer, Logger, LogLevel } from '@livestore/utils/effect'
4
+ import { Console, Effect, FetchHttpClient, Layer, Logger, References } from '@livestore/utils/effect'
5
5
  import { Cli, PlatformNode } from '@livestore/utils/node'
6
6
 
7
7
  import { command } from './cli.ts'
8
8
 
9
9
  const cli = Cli.Command.run(command, {
10
- name: 'livestore',
11
10
  version: liveStoreVersion,
12
11
  })
13
12
 
14
- const showExperimentalWarning = Console.log('⚠️ Warning: LiveStore CLI is experimental and under active development')
13
+ const showExperimentalWarning = Console.error('⚠️ Warning: LiveStore CLI is experimental and under active development')
15
14
 
16
15
  const layer = Layer.mergeAll(
17
- PlatformNode.NodeContext.layer,
16
+ PlatformNode.NodeServices.layer,
18
17
  FetchHttpClient.layer,
19
- Logger.minimumLogLevel(LogLevel.Info),
18
+ Layer.succeed(References.MinimumLogLevel, 'Info'),
19
+ // Keep stdout available for command data and protocol transports such as MCP stdio.
20
+ Layer.succeed(Logger.LogToStderr, true),
20
21
  )
21
22
 
22
23
  Effect.gen(function* () {
23
24
  yield* showExperimentalWarning
24
- return yield* cli(process.argv)
25
+ return yield* cli
25
26
  }).pipe(Effect.provide(layer), PlatformNode.NodeRuntime.runMain)
package/src/cli.ts CHANGED
@@ -5,5 +5,5 @@ import { mcpCommand } from './commands/mcp.ts'
5
5
  import { createCommand } from './commands/new-project.ts'
6
6
 
7
7
  export const command = Cli.Command.make('livestore', {
8
- verbose: Cli.Options.boolean('verbose').pipe(Cli.Options.withDefault(false)),
8
+ verbose: Cli.Flag.Boolean('verbose').pipe(Cli.Flag.withDefault(false)),
9
9
  }).pipe(Cli.Command.withSubcommands([mcpCommand, createCommand, syncCommand]))
@@ -0,0 +1,12 @@
1
+ import { McpServer } from 'effect/ai'
2
+
3
+ import { Effect, Layer } from '@livestore/utils/effect'
4
+ import { PlatformNode } from '@livestore/utils/node'
5
+
6
+ import { supportedMcpProtocols } from '../mcp-protocols.ts'
7
+
8
+ McpServer.layerStdio({
9
+ name: 'livestore-mcp-protocol-test',
10
+ version: '0.0.0',
11
+ protocols: supportedMcpProtocols,
12
+ }).pipe(Layer.provide(PlatformNode.NodeStdio.layer), Layer.launch, Effect.scoped, PlatformNode.NodeRuntime.runMain)
@@ -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/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.Finite.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
  }),
@@ -39,7 +39,7 @@ export const coachTool = Tool.make('livestore_coach', {
39
39
 
40
40
  /** Composed layer: HttpClient → OpenAiClient → LanguageModel */
41
41
  const LanguageModelLayer = OpenAiLanguageModel.layer({ model: 'gpt-4o-mini' }).pipe(
42
- Layer.provide(OpenAiClient.layerConfig({ apiKey: Config.redacted('OPENAI_API_KEY') })),
42
+ Layer.provide(OpenAiClient.layerConfig({ apiKey: Config.Redacted('OPENAI_API_KEY') })),
43
43
  Layer.provide(FetchHttpClient.layer),
44
44
  )
45
45
 
@@ -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/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/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/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
+ })