@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixtures/mock-config.d.ts +7 -9
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-config.js +6 -6
- package/dist/__tests__/fixtures/mock-config.js.map +1 -1
- package/dist/__tests__/sync-operations.test.js +20 -20
- package/dist/__tests__/sync-operations.test.js.map +1 -1
- package/dist/bin.js +6 -5
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +2 -23
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- 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 +5 -19
- package/dist/commands/import-export.d.ts.map +1 -1
- package/dist/commands/import-export.js +12 -12
- package/dist/commands/import-export.js.map +1 -1
- package/dist/commands/mcp-coach.d.ts +8 -7
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +10 -10
- 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 +1 -1
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
- package/dist/commands/mcp-tool-handlers.js +1 -0
- package/dist/commands/mcp-tool-handlers.js.map +1 -1
- package/dist/commands/mcp-tools-defs.d.ts +65 -64
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
- package/dist/commands/mcp-tools-defs.js +59 -52
- package/dist/commands/mcp-tools-defs.js.map +1 -1
- 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 +5 -4
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +19 -27
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +17 -16
- 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 +2 -2
- package/dist/mcp-runtime/runtime.d.ts.map +1 -1
- package/dist/mcp-runtime/runtime.js +4 -4
- package/dist/mcp-runtime/runtime.js.map +1 -1
- package/dist/module-loader.d.ts +2 -2
- package/dist/module-loader.d.ts.map +1 -1
- package/dist/module-loader.js +6 -6
- package/dist/module-loader.js.map +1 -1
- package/dist/sync-operations.d.ts +24 -30
- package/dist/sync-operations.d.ts.map +1 -1
- package/dist/sync-operations.js +10 -12
- package/dist/sync-operations.js.map +1 -1
- package/package.json +30 -44
- package/src/__tests__/fixtures/mock-config.ts +8 -8
- package/src/__tests__/sync-operations.test.ts +21 -35
- package/src/bin.ts +7 -6
- package/src/cli.ts +1 -1
- package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
- package/src/commands/import-export.ts +29 -29
- package/src/commands/mcp-coach.ts +12 -11
- 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-tool-handlers.ts +11 -8
- package/src/commands/mcp-tools-defs.test.ts +13 -0
- package/src/commands/mcp-tools-defs.ts +53 -51
- package/src/commands/mcp.ts +7 -4
- package/src/commands/new-project.ts +49 -36
- 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 +7 -7
- package/src/module-loader.ts +10 -10
- 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 =
|
|
10
|
-
const jsonParse = Schema.decodeUnknownSync(Schema.
|
|
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.
|
|
175
|
-
Cli.
|
|
176
|
-
Cli.
|
|
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.
|
|
179
|
-
Cli.
|
|
180
|
-
Cli.
|
|
178
|
+
storeId: Cli.Flag.string('store-id').pipe(
|
|
179
|
+
Cli.Flag.withAlias('i'),
|
|
180
|
+
Cli.Flag.withDescription('Store identifier'),
|
|
181
181
|
),
|
|
182
|
-
clientId: Cli.
|
|
183
|
-
Cli.
|
|
184
|
-
Cli.
|
|
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.
|
|
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.
|
|
222
|
-
Cli.
|
|
223
|
-
Cli.
|
|
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.
|
|
226
|
-
Cli.
|
|
227
|
-
Cli.
|
|
225
|
+
storeId: Cli.Flag.string('store-id').pipe(
|
|
226
|
+
Cli.Flag.withAlias('i'),
|
|
227
|
+
Cli.Flag.withDescription('Store identifier'),
|
|
228
228
|
),
|
|
229
|
-
clientId: Cli.
|
|
230
|
-
Cli.
|
|
231
|
-
Cli.
|
|
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.
|
|
234
|
-
Cli.
|
|
235
|
-
Cli.
|
|
236
|
-
Cli.
|
|
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.
|
|
239
|
-
Cli.
|
|
240
|
-
Cli.
|
|
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.
|
|
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 '
|
|
1
|
+
import { AiError, LanguageModel, Prompt, Tool } from 'effect/unstable/ai'
|
|
2
2
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'
|
|
3
3
|
|
|
4
|
-
import {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
30
|
+
Schema.String.annotate({
|
|
31
31
|
description: 'Specific actionable suggestions for improvement',
|
|
32
32
|
}),
|
|
33
|
-
).
|
|
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 =
|
|
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 {
|
|
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 =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
24
|
-
explanation: Schema.String.
|
|
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.
|
|
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.
|
|
36
|
-
description: Schema.String.
|
|
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.
|
|
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.
|
|
95
|
+
storeId: Schema.String.annotate({ description: 'Required store id for the LiveStore instance.' }),
|
|
94
96
|
clientId: Schema.optional(
|
|
95
|
-
Schema.String.
|
|
97
|
+
Schema.String.annotate({ description: 'Optional client id for the LiveStore instance.' }),
|
|
96
98
|
),
|
|
97
99
|
sessionId: Schema.optional(
|
|
98
|
-
Schema.String.
|
|
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).
|
|
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).
|
|
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.
|
|
142
|
-
bindValues: Schema.Union(
|
|
143
|
-
Schema.Array(Schema.
|
|
144
|
-
Schema.Record(
|
|
145
|
-
).
|
|
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(
|
|
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.
|
|
183
|
-
args: Schema.
|
|
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(
|
|
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.
|
|
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.
|
|
261
|
-
clientId: Schema.optional(Schema.String.
|
|
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.
|
|
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.
|
|
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.
|
|
303
|
-
clientId: Schema.optional(Schema.String.
|
|
304
|
-
data: Schema.
|
|
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.
|
|
310
|
+
Schema.Boolean.annotate({ description: 'Force import even if store ID does not match' }),
|
|
309
311
|
),
|
|
310
|
-
dryRun: Schema.optional(Schema.Boolean.
|
|
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,
|
package/src/commands/mcp.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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.
|
|
94
|
+
Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])),
|
|
95
|
+
Layer.provide(PlatformNode.NodeStdio.layer),
|
|
93
96
|
Layer.launch,
|
|
94
97
|
)
|
|
95
98
|
}),
|