Skip to content

Commit f160600

Browse files
authored
Merge pull request #7 from railsware/CPL-20411/use-execution-id
[CPL-20411] use execution ID
2 parents 1c3d826 + 0102a30 commit f160600

File tree

14 files changed

+156
-118
lines changed

14 files changed

+156
-118
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ Remove this line, if you're offline or if you specifically want to use the image
4343

4444
## Tools
4545
### Data flows
46-
- **get-data** - Gets the result of a data flow run as a SQLite file and executes a read-only query on it.
46+
- **get-data** - Gets the result of a data flow run as a SQLite file and executes a read-only query on it. Currently, only data flows built from a dashboard or dataset template are supported.
4747
- `dataflowId`: Data flow ID (`string`, **required**)
48+
- `executionId`: Data flow run ID (`string`, **required**)
4849
- `query`: Query to run on the data flow SQLite file (`string`, **required**)
4950

50-
- **get-schema** - Gets the data flow schema file.
51+
- **get-schema** - Gets the data flow schema file. Currently, only data flows built from a dashboard or dataset template are supported.
5152
- `dataflowId`: Data flow ID (`string`, **required**)
53+
- `executionId`: Data flow run ID (`string`, **required**)
5254

5355
## Development
5456

package-lock.json

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
"dependencies": {
1717
"@modelcontextprotocol/sdk": "1.11.4",
1818
"better-sqlite3": "11.10.0",
19+
"json-schema-to-zod": "2.6.1",
1920
"lodash": "4.17.21",
2021
"pino": "9.7.0",
2122
"tsx": "4.19.4",
2223
"url-template": "3.1.1",
2324
"znv": "0.5.0",
24-
"zod": "3.24.4"
25+
"zod": "3.24.4",
26+
"zod-to-json-schema": "3.24.5",
27+
"zod-validation-error": "3.4.1"
2528
},
2629
"devDependencies": {
2730
"@eslint/js": "9.27.0",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface SignedUrlDto {
2+
file: 'sqlite' | 'schema',
3+
signed_url: string,
4+
}

src/tools/get-data/handler.test.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('getData', () => {
5050

5151
const toolResult = await handler({
5252
dataflowId: 'test-dataflow-id',
53+
executionId: 'test-execution-id',
5354
query: 'SELECT * FROM data'
5455
})
5556

@@ -69,6 +70,7 @@ describe('getData', () => {
6970

7071
const toolResult = await handler({
7172
dataflowId: 'test-dataflow-id',
73+
executionId: 'test-execution-id',
7274
query: 'SELECT * FROM data'
7375
})
7476

@@ -89,6 +91,7 @@ describe('getData', () => {
8991

9092
const toolResult = await handler({
9193
dataflowId: 'test-dataflow-id',
94+
executionId: 'test-execution-id',
9295
query: 'SELECT * FROM data'
9396
})
9497

@@ -108,6 +111,7 @@ describe('getData', () => {
108111

109112
const toolResult = await handler({
110113
dataflowId: 'test-dataflow-id',
114+
executionId: 'test-execution-id',
111115
query: 'SELECT INVALID SQL QUERY'
112116
})
113117

@@ -129,7 +133,7 @@ describe('with invalid params', () => {
129133
isError: true,
130134
content: [{
131135
type: 'text',
132-
text: 'This tool requires parameters'
136+
text: 'Invalid parameters for get-data tool. Validation error: Required'
133137
}]
134138
})
135139
})
@@ -141,34 +145,35 @@ describe('with invalid params', () => {
141145
isError: true,
142146
content: [{
143147
type: 'text',
144-
text: 'Missing or invalid required parameter: dataflowId must be a non-empty string'
148+
text: 'Invalid parameters for get-data tool. Validation error: Required at "dataflowId"; Required at "executionId"'
145149
}]
146150
})
147151
})
148152

149153
it('returns error on missing query', async () => {
150-
const toolResult = await handler({ dataflowId: 'test-dataflow-id' })
154+
const toolResult = await handler({ dataflowId: 'test-dataflow-id', executionId: 'test-execution-id' })
151155

152156
expect(toolResult).toEqual({
153157
isError: true,
154158
content: [{
155159
type: 'text',
156-
text: 'Missing or invalid required parameter: query must be a non-empty string'
160+
text: 'Invalid parameters for get-data tool. Validation error: Required at "query"'
157161
}]
158162
})
159163
})
160164

161165
it('returns error on invalid dataflowId', async () => {
162166
const toolResult = await handler({
163167
dataflowId: 123,
168+
executionId: true,
164169
query: 'SELECT * FROM data'
165170
})
166171

167172
expect(toolResult).toEqual({
168173
isError: true,
169174
content: [{
170175
type: 'text',
171-
text: 'Missing or invalid required parameter: dataflowId must be a non-empty string'
176+
text: 'Invalid parameters for get-data tool. Validation error: Expected string, received number at "dataflowId"; Expected string, received boolean at "executionId"'
172177
}]
173178
})
174179
})
@@ -183,22 +188,23 @@ describe('with invalid params', () => {
183188
isError: true,
184189
content: [{
185190
type: 'text',
186-
text: 'Missing or invalid required parameter: query must be a non-empty string'
191+
text: 'Invalid parameters for get-data tool. Validation error: Required at "executionId"; Expected string, received number at "query"'
187192
}]
188193
})
189194
})
190195

191196
it('returns error on non-SELECT query', async () => {
192197
const toolResult = await handler({
193198
dataflowId: 'test-dataflow-id',
199+
executionId: 'test-execution-id',
194200
query: 'INSERT INTO data (col_0, col_1) VALUES (2, "Test 2")'
195201
})
196202

197203
expect(toolResult).toEqual({
198204
isError: true,
199205
content: [{
200206
type: 'text',
201-
text: 'Missing or invalid required parameter: query must start with SELECT'
207+
text: 'Invalid parameters for get-data tool. Validation error: must start with "SELECT" at "query"'
202208
}]
203209
})
204210
})

src/tools/get-data/handler.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
import Database from 'better-sqlite3'
22
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
3+
import { fromError } from 'zod-validation-error'
34

5+
import { logger } from '@/logger'
46
import { textResponse } from '@/util/tool-response'
57
import { FileManager } from '@/tools/shared/file-manager'
68

7-
import { validateParams } from './validate-params'
9+
import { zodSchema } from './input-schema'
810

911
export const handler = async (params?: Record<string, unknown>): Promise<CallToolResult> => {
10-
try {
11-
validateParams(params)
12-
} catch (e) {
12+
const validationResult = zodSchema.safeParse(params)
13+
14+
if (!validationResult.success) {
15+
const error = fromError(validationResult.error)
16+
logger.error(`Invalid parameters for get-data tool: ${error.toString()}`)
17+
1318
return textResponse({
14-
text: (e as Error).message,
19+
text: `Invalid parameters for get-data tool. ${error.toString()}`,
1520
isError: true,
1621
})
1722
}
1823

19-
const dataflowId = params!.dataflowId as string
20-
const query = params!.query as string
21-
22-
const fileManager = new FileManager({ dataflowId })
24+
const fileManager = new FileManager(validationResult.data)
2325

2426
let sqlitePath: string
2527
try {
2628
sqlitePath = await fileManager.getFile('sqlite')
2729
} catch (e) {
28-
return textResponse({ text: `Failed to get data flow ${dataflowId} sqlite file. ${e}`, isError: true })
30+
return textResponse({ text: `Failed to get data flow ${validationResult.data.dataflowId} sqlite file. ${e}`, isError: true })
2931
}
3032

3133
const db = new Database(sqlitePath)
3234
let statement, queryResult
3335
try {
34-
statement = db.prepare(query)
36+
statement = db.prepare(validationResult.data.query)
3537
queryResult = statement.all()
3638
} catch (e) {
3739
return textResponse({ text: `Failed to execute query: ${e}`, isError: true })

src/tools/get-data/input-schema.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
export const inputSchema = {
2-
type: 'object',
3-
properties: {
4-
dataflowId: {
5-
type: 'string',
6-
description: 'The ID of the data flow with a successful run',
7-
pattern: '^\\S+'
8-
},
9-
query: {
10-
type: 'string',
11-
description: 'The SQL query to run on the data flow sqlite file',
12-
pattern: '^SELECT.*?'
13-
}
14-
},
15-
required: ['dataflowId', 'query'],
16-
}
1+
import { z } from 'zod'
2+
import { zodToJsonSchema } from 'zod-to-json-schema'
3+
4+
export const zodSchema = z.object({
5+
dataflowId: z.string()
6+
.min(1, 'dataflowId is required')
7+
.regex(/^\S+$/, 'dataflowId must not contain whitespace')
8+
.describe('The ID of the data flow with a successful run'),
9+
executionId: z.string()
10+
.min(1, 'executionId is required')
11+
.regex(/^\S+$/, 'executionId must be a non-empty string')
12+
.describe('The ID of the last successful run (execution) of the data flow.'),
13+
query: z.string()
14+
.min(1, 'query is required')
15+
.regex(/^SELECT.*?/, 'must start with "SELECT"')
16+
.describe('The SQL query to run on the data flow sqlite file.'),
17+
}).strict()
18+
19+
export const inputSchema = zodToJsonSchema(zodSchema)

src/tools/get-data/validate-params.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/tools/get-schema/handler.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const mockSchema = {
1818
// Response mocks
1919
const mockCreateSignedUrl = createMockResponse(
2020
async () => new Response(
21-
JSON.stringify({ file: 'schema', url: 'https://example.com/schema.json' })
21+
JSON.stringify({ file: 'schema', signed_url: 'https://example.com/schema.json' })
2222
)
2323
)
2424
const mockGetSchemaFile = createMockResponse(
@@ -48,7 +48,7 @@ describe('getSchema', () => {
4848
.mockImplementationOnce(mockCreateSignedUrl)
4949
.mockImplementationOnce(mockGetSchemaFile)
5050

51-
const toolResult = await handler({ dataflowId: 'test-dataflow-id' })
51+
const toolResult = await handler({ dataflowId: 'test-dataflow-id', executionId: 'test-execution-id' })
5252

5353
expect(toolResult).toEqual({
5454
isError: false,
@@ -70,7 +70,7 @@ describe('getSchema', () => {
7070
.mockImplementationOnce(mockCreateSignedUrlError)
7171
.mockImplementationOnce(mockGetSchemaFile)
7272

73-
const toolResult = await handler({ dataflowId: 'test-dataflow-id' })
73+
const toolResult = await handler({ dataflowId: 'test-dataflow-id', executionId: 'test-execution-id' })
7474

7575
expect(toolResult).toEqual({
7676
isError: true,
@@ -88,7 +88,7 @@ describe('getSchema', () => {
8888
.mockImplementationOnce(mockCreateSignedUrl)
8989
.mockImplementationOnce(mockGetSchemaFileError)
9090

91-
const toolResult = await handler({ dataflowId: 'test-dataflow-id' })
91+
const toolResult = await handler({ dataflowId: 'test-dataflow-id', executionId: 'test-execution-id' })
9292

9393
expect(toolResult).toEqual({
9494
isError: true,
@@ -109,7 +109,7 @@ describe('with invalid params', () => {
109109
isError: true,
110110
content: [{
111111
type: 'text',
112-
text: 'This tool requires parameters',
112+
text: 'Invalid parameters for get-schema tool. Validation error: Required',
113113
}]
114114
})
115115
})
@@ -121,19 +121,19 @@ describe('with invalid params', () => {
121121
isError: true,
122122
content: [{
123123
type: 'text',
124-
text: 'Missing or invalid required parameter: dataflowId must be a non-empty string',
124+
text: 'Invalid parameters for get-schema tool. Validation error: Required at "dataflowId"; Required at "executionId"',
125125
}]
126126
})
127127
})
128128

129129
it('returns error on invalid dataflowId', async () => {
130-
const toolResult = await handler({ dataflowId: 123 })
130+
const toolResult = await handler({ dataflowId: 123, executionId: true })
131131

132132
expect(toolResult).toEqual({
133133
isError: true,
134134
content: [{
135135
type: 'text',
136-
text: 'Missing or invalid required parameter: dataflowId must be a non-empty string',
136+
text: 'Invalid parameters for get-schema tool. Validation error: Expected string, received number at "dataflowId"; Expected string, received boolean at "executionId"',
137137
}]
138138
})
139139
})

0 commit comments

Comments
 (0)