Skip to content

Commit 7383b5c

Browse files
authored
Merge pull request #64 from cloudflare/d1-tools
feat: D1 database tools
2 parents 43bb754 + 6d35e54 commit 7383b5c

File tree

8 files changed

+926
-679
lines changed

8 files changed

+926
-679
lines changed

apps/workers-bindings/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
handleTokenExchangeCallback,
99
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
1010
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
11+
import { registerD1Tools } from '@repo/mcp-common/src/tools/d1'
1112
import { registerKVTools } from '@repo/mcp-common/src/tools/kv_namespace'
1213
import { registerR2BucketTools } from '@repo/mcp-common/src/tools/r2_bucket'
1314
import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker'
@@ -39,6 +40,7 @@ export class WorkersBindingsMCP extends McpAgent<Env, WorkersBindingsMCPState, P
3940
registerKVTools(this)
4041
registerWorkersTools(this)
4142
registerR2BucketTools(this)
43+
registerD1Tools(this)
4244
}
4345
getActiveAccountId() {
4446
// TODO: Figure out why this fail sometimes, and why we need to wrap this in a try catch
@@ -68,6 +70,7 @@ const BindingsScopes = {
6870
'workers:write':
6971
'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
7072
'workers_observability:read': 'See observability logs for your account',
73+
'd1:write': 'Create, read, and write to D1 databases',
7174
offline_access: 'Grants refresh tokens for long-lived access.',
7275
} as const
7376

packages/mcp-common/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
2+
3+
export const MISSING_ACCOUNT_ID_RESPONSE = {
4+
content: [
5+
{
6+
type: 'text',
7+
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
8+
},
9+
],
10+
} satisfies CallToolResult

packages/mcp-common/src/tools/d1.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { z } from 'zod'
2+
3+
import { getCloudflareClient } from '../cloudflare-api'
4+
import { MISSING_ACCOUNT_ID_RESPONSE } from '../constants'
5+
import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent'
6+
import {
7+
D1DatabaseNameParam,
8+
D1DatabasePrimaryLocationHintParam,
9+
D1DatabaseQueryParamsParam,
10+
D1DatabaseQuerySqlParam,
11+
} from '../types/d1'
12+
import { PaginationPageParam, PaginationPerPageParam } from '../types/shared'
13+
14+
export function registerD1Tools(agent: CloudflareMcpAgent) {
15+
agent.server.tool(
16+
'd1_databases_list',
17+
'List all of the D1 databases in your Cloudflare account',
18+
{
19+
name: D1DatabaseNameParam.nullable().optional(),
20+
page: PaginationPageParam,
21+
per_page: PaginationPerPageParam,
22+
},
23+
async ({ name, page, per_page }) => {
24+
const account_id = agent.getActiveAccountId()
25+
if (!account_id) {
26+
return MISSING_ACCOUNT_ID_RESPONSE
27+
}
28+
try {
29+
const client = getCloudflareClient(agent.props.accessToken)
30+
const listResponse = await client.d1.database.list({
31+
account_id,
32+
name: name ?? undefined,
33+
page: page ?? undefined,
34+
per_page: per_page ?? undefined,
35+
})
36+
37+
return {
38+
content: [
39+
{
40+
type: 'text',
41+
text: JSON.stringify({
42+
result: listResponse.result,
43+
result_info: listResponse.result_info,
44+
}),
45+
},
46+
],
47+
}
48+
} catch (error) {
49+
return {
50+
content: [
51+
{
52+
type: 'text',
53+
text: `Error listing D1 databases: ${error instanceof Error && error.message}`,
54+
},
55+
],
56+
}
57+
}
58+
}
59+
)
60+
61+
agent.server.tool(
62+
'd1_database_create',
63+
'Create a new D1 database in your Cloudflare account',
64+
{
65+
name: D1DatabaseNameParam,
66+
primary_location_hint: D1DatabasePrimaryLocationHintParam.nullable().optional(),
67+
},
68+
async ({ name, primary_location_hint }) => {
69+
const account_id = agent.getActiveAccountId()
70+
if (!account_id) {
71+
return MISSING_ACCOUNT_ID_RESPONSE
72+
}
73+
try {
74+
const client = getCloudflareClient(agent.props.accessToken)
75+
const d1Database = await client.d1.database.create({
76+
account_id,
77+
name,
78+
primary_location_hint: primary_location_hint ?? undefined,
79+
})
80+
81+
return {
82+
content: [
83+
{
84+
type: 'text',
85+
text: JSON.stringify(d1Database),
86+
},
87+
],
88+
}
89+
} catch (error) {
90+
return {
91+
content: [
92+
{
93+
type: 'text',
94+
text: `Error creating D1 database: ${error instanceof Error && error.message}`,
95+
},
96+
],
97+
}
98+
}
99+
}
100+
)
101+
102+
agent.server.tool(
103+
'd1_database_delete',
104+
'Delete a d1 database in your Cloudflare account',
105+
{ database_id: z.string() },
106+
async ({ database_id }) => {
107+
const account_id = agent.getActiveAccountId()
108+
if (!account_id) {
109+
return MISSING_ACCOUNT_ID_RESPONSE
110+
}
111+
try {
112+
const client = getCloudflareClient(agent.props.accessToken)
113+
const deleteResponse = await client.d1.database.delete(database_id, {
114+
account_id,
115+
})
116+
return {
117+
content: [
118+
{
119+
type: 'text',
120+
text: JSON.stringify(deleteResponse),
121+
},
122+
],
123+
}
124+
} catch (error) {
125+
return {
126+
content: [
127+
{
128+
type: 'text',
129+
text: `Error deleting D1 database: ${error instanceof Error && error.message}`,
130+
},
131+
],
132+
}
133+
}
134+
}
135+
)
136+
137+
agent.server.tool(
138+
'd1_database_get',
139+
'Get a D1 database in your Cloudflare account',
140+
{ database_id: z.string() },
141+
async ({ database_id }) => {
142+
const account_id = agent.getActiveAccountId()
143+
if (!account_id) {
144+
return MISSING_ACCOUNT_ID_RESPONSE
145+
}
146+
try {
147+
const client = getCloudflareClient(agent.props.accessToken)
148+
const d1Database = await client.d1.database.get(database_id, {
149+
account_id,
150+
})
151+
152+
return {
153+
content: [
154+
{
155+
type: 'text',
156+
text: JSON.stringify(d1Database),
157+
},
158+
],
159+
}
160+
} catch (error) {
161+
return {
162+
content: [
163+
{
164+
type: 'text',
165+
text: `Error getting D1 database: ${error instanceof Error && error.message}`,
166+
},
167+
],
168+
}
169+
}
170+
}
171+
)
172+
173+
agent.server.tool(
174+
'd1_database_query',
175+
'Query a D1 database in your Cloudflare account',
176+
{
177+
database_id: z.string(),
178+
sql: D1DatabaseQuerySqlParam,
179+
params: D1DatabaseQueryParamsParam.nullable(),
180+
},
181+
async ({ database_id, sql, params }) => {
182+
const account_id = agent.getActiveAccountId()
183+
if (!account_id) {
184+
return MISSING_ACCOUNT_ID_RESPONSE
185+
}
186+
try {
187+
const client = getCloudflareClient(agent.props.accessToken)
188+
const queryResult = await client.d1.database.query(database_id, {
189+
account_id,
190+
sql,
191+
params: params ?? undefined,
192+
})
193+
return {
194+
content: [
195+
{
196+
type: 'text',
197+
text: JSON.stringify(queryResult.result),
198+
},
199+
],
200+
}
201+
} catch (error) {
202+
return {
203+
content: [
204+
{
205+
type: 'text',
206+
text: `Error querying D1 database: ${error instanceof Error && error.message}`,
207+
},
208+
],
209+
}
210+
}
211+
}
212+
)
213+
}

packages/mcp-common/src/tools/kv_namespace.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1-
import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js'
21
import { z } from 'zod'
32

43
import { getCloudflareClient } from '../cloudflare-api'
4+
import { MISSING_ACCOUNT_ID_RESPONSE } from '../constants'
55
import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent'
66
import {
77
KvNamespaceIdSchema,
88
KvNamespacesListParamsSchema,
99
KvNamespaceTitleSchema,
1010
} from '../types/kv_namespace'
1111

12-
// Define the standard response for missing account ID
13-
const MISSING_ACCOUNT_ID_RESPONSE = {
14-
content: [
15-
{
16-
type: 'text',
17-
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
18-
},
19-
],
20-
} satisfies CallToolResult
21-
2212
export function registerKVTools(agent: CloudflareMcpAgent) {
2313
/**
2414
* Tool to list KV namespaces.

0 commit comments

Comments
 (0)