@gokiteam/goki-dev 0.2.2 → 0.2.3
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/cli/NgrokManager.js +20 -6
- package/package.json +1 -1
- package/src/api/firestore/Controllers.js +14 -0
- package/src/api/firestore/Logic.js +74 -1
- package/src/api/firestore/Router.js +2 -0
- package/src/api/firestore/Schemas.js +160 -0
- package/src/mcp/tools/data.js +8 -3
- package/src/mcp/tools/docker.js +6 -2
- package/src/mcp/tools/firestore.js +49 -2
- package/src/mcp/tools/functions.js +15 -4
- package/src/mcp/tools/httpTraffic.js +6 -1
- package/src/mcp/tools/logging.js +10 -1
- package/src/mcp/tools/mqtt.js +2 -0
- package/src/mcp/tools/postgres.js +9 -1
- package/src/mcp/tools/pubsub.js +18 -4
- package/src/mcp/tools/redis.js +10 -2
- package/src/mcp/tools/services.js +11 -3
- package/src/mcp/tools/snapshots.js +6 -2
- package/src/mcp/tools/webhooks.js +5 -1
package/cli/NgrokManager.js
CHANGED
|
@@ -9,6 +9,7 @@ const NGROK_API = 'http://localhost:4040'
|
|
|
9
9
|
const DATA_DIR = path.join(os.homedir(), '.goki-dev', 'data')
|
|
10
10
|
const PID_FILE = path.join(DATA_DIR, 'ngrok.pid')
|
|
11
11
|
const STATE_FILE = path.join(DATA_DIR, 'ngrok.json')
|
|
12
|
+
const DOMAIN_FILE = path.join(DATA_DIR, 'ngrok-domain.txt')
|
|
12
13
|
|
|
13
14
|
export class NgrokManager {
|
|
14
15
|
// --- PID File Management ---
|
|
@@ -301,6 +302,12 @@ export class NgrokManager {
|
|
|
301
302
|
// --- Domain Management ---
|
|
302
303
|
|
|
303
304
|
static async loadDomain () {
|
|
305
|
+
// Check local domain file first (most reliable, survives tunnel failures)
|
|
306
|
+
try {
|
|
307
|
+
const domain = fs.readFileSync(DOMAIN_FILE, 'utf8').trim()
|
|
308
|
+
if (domain) return domain
|
|
309
|
+
} catch { /* file doesn't exist yet */ }
|
|
310
|
+
// Try dev-tools API
|
|
304
311
|
try {
|
|
305
312
|
const response = await fetch(`${DEV_TOOLS_API}/v1/webhooks/settings/get`, {
|
|
306
313
|
method: 'POST',
|
|
@@ -311,17 +318,24 @@ export class NgrokManager {
|
|
|
311
318
|
if (response.ok) {
|
|
312
319
|
const result = await response.json()
|
|
313
320
|
const domain = result.data?.settings?.ngrok_domain
|
|
314
|
-
if (domain)
|
|
321
|
+
if (domain) {
|
|
322
|
+
// Persist locally for next time
|
|
323
|
+
this.ensureDataDir()
|
|
324
|
+
fs.writeFileSync(DOMAIN_FILE, domain.trim(), 'utf8')
|
|
325
|
+
return domain
|
|
326
|
+
}
|
|
315
327
|
}
|
|
316
|
-
} catch {
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
// Fallback: read from local state file
|
|
328
|
+
} catch { /* API not available */ }
|
|
329
|
+
// Last resort: check tunnel state file
|
|
320
330
|
const state = this.readState()
|
|
321
331
|
return state?.domain || null
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
static async saveDomain (domain) {
|
|
335
|
+
// Always save locally so it survives tunnel failures and API downtime
|
|
336
|
+
this.ensureDataDir()
|
|
337
|
+
fs.writeFileSync(DOMAIN_FILE, domain.trim(), 'utf8')
|
|
338
|
+
// Also save to dev-tools API (best-effort)
|
|
325
339
|
try {
|
|
326
340
|
await fetch(`${DEV_TOOLS_API}/v1/webhooks/settings/update`, {
|
|
327
341
|
method: 'POST',
|
|
@@ -332,7 +346,7 @@ export class NgrokManager {
|
|
|
332
346
|
signal: AbortSignal.timeout(3000)
|
|
333
347
|
})
|
|
334
348
|
} catch (error) {
|
|
335
|
-
Logger.warn(`Failed to save ngrok domain: ${error.message}`)
|
|
349
|
+
Logger.warn(`Failed to save ngrok domain to API: ${error.message}`)
|
|
336
350
|
}
|
|
337
351
|
}
|
|
338
352
|
|
package/package.json
CHANGED
|
@@ -72,6 +72,20 @@ export const Controllers = {
|
|
|
72
72
|
ctx.reply(result)
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
+
async setDocument (ctx) {
|
|
76
|
+
const { traceId } = ctx.state
|
|
77
|
+
const { collectionPath, documentId, fields, projectId } = ctx.request.body
|
|
78
|
+
const result = await Logic.setDocument({ collectionPath, documentId, fields, projectId, traceId })
|
|
79
|
+
ctx.reply(result)
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async createBatch (ctx) {
|
|
83
|
+
const { traceId } = ctx.state
|
|
84
|
+
const { collectionPath, documents, projectId } = ctx.request.body
|
|
85
|
+
const result = await Logic.createBatch({ collectionPath, documents, projectId, traceId })
|
|
86
|
+
ctx.reply(result)
|
|
87
|
+
},
|
|
88
|
+
|
|
75
89
|
async executeQuery (ctx) {
|
|
76
90
|
const { traceId } = ctx.state
|
|
77
91
|
const { collectionPath, where, orderBy, limit, projectId } = ctx.request.body
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { initializeApp, getApps
|
|
1
|
+
import { initializeApp, getApps } from 'firebase-admin/app'
|
|
2
2
|
import { getFirestore } from 'firebase-admin/firestore'
|
|
3
3
|
import { SqliteStore } from '../../singletons/SqliteStore.js'
|
|
4
4
|
import { Application } from '../../configs/Application.js'
|
|
@@ -354,6 +354,79 @@ export const Logic = {
|
|
|
354
354
|
}
|
|
355
355
|
},
|
|
356
356
|
|
|
357
|
+
async setDocument (params) {
|
|
358
|
+
const { collectionPath, documentId, fields, traceId } = params
|
|
359
|
+
const projectId = resolveProjectId(params)
|
|
360
|
+
try {
|
|
361
|
+
const url = `${FIRESTORE_API}/v1/projects/${projectId}/databases/${DATABASE}/documents/${collectionPath}/${documentId}`
|
|
362
|
+
const response = await fetch(url, {
|
|
363
|
+
method: 'PATCH',
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify({ fields })
|
|
366
|
+
})
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`Firestore API error: ${response.statusText}`)
|
|
369
|
+
}
|
|
370
|
+
const document = await response.json()
|
|
371
|
+
trackCollection(collectionPath)
|
|
372
|
+
return {
|
|
373
|
+
document,
|
|
374
|
+
traceId
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
status: 'error',
|
|
379
|
+
message: error.message,
|
|
380
|
+
traceId
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async createBatch (params) {
|
|
386
|
+
const { collectionPath, documents = [], traceId } = params
|
|
387
|
+
const projectId = resolveProjectId(params)
|
|
388
|
+
const BATCH_SIZE = 20
|
|
389
|
+
try {
|
|
390
|
+
const failedIds = []
|
|
391
|
+
let createdCount = 0
|
|
392
|
+
for (let i = 0; i < documents.length; i += BATCH_SIZE) {
|
|
393
|
+
const batch = documents.slice(i, i + BATCH_SIZE)
|
|
394
|
+
const results = await Promise.allSettled(
|
|
395
|
+
batch.map(doc =>
|
|
396
|
+
this.createDocument({
|
|
397
|
+
collectionPath,
|
|
398
|
+
projectId,
|
|
399
|
+
documentId: doc.documentId,
|
|
400
|
+
fields: doc.fields,
|
|
401
|
+
traceId
|
|
402
|
+
})
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
for (let j = 0; j < results.length; j++) {
|
|
406
|
+
const result = results[j]
|
|
407
|
+
if (result.status === 'fulfilled' && !result.value.status) {
|
|
408
|
+
createdCount++
|
|
409
|
+
} else {
|
|
410
|
+
const docId = batch[j].documentId || `index-${i + j}`
|
|
411
|
+
failedIds.push(docId)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
success: true,
|
|
417
|
+
createdCount,
|
|
418
|
+
failedIds,
|
|
419
|
+
traceId
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
status: 'error',
|
|
424
|
+
message: error.message,
|
|
425
|
+
traceId
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
|
|
357
430
|
async clearCollection (params) {
|
|
358
431
|
const { collectionPath, projectId, traceId } = params
|
|
359
432
|
try {
|
|
@@ -11,6 +11,8 @@ v1.post('/documents/get', Controllers.getDocument)
|
|
|
11
11
|
v1.post('/documents/create', Controllers.createDocument)
|
|
12
12
|
v1.post('/documents/update', Controllers.updateDocument)
|
|
13
13
|
v1.post('/documents/delete', Controllers.deleteDocument)
|
|
14
|
+
v1.post('/documents/set', Controllers.setDocument)
|
|
15
|
+
v1.post('/documents/create-batch', Controllers.createBatch)
|
|
14
16
|
v1.post('/documents/delete-by-query', Controllers.deleteByQuery)
|
|
15
17
|
v1.post('/documents/delete-by-prefix', Controllers.deleteByPrefix)
|
|
16
18
|
v1.post('/documents/delete-batch', Controllers.deleteBatch)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Joi } from '@gokiteam/koa'
|
|
2
|
+
|
|
3
|
+
const whereClause = Joi.object({
|
|
4
|
+
field: Joi.string().required(),
|
|
5
|
+
operator: Joi.string().valid('==', '!=', '<', '<=', '>', '>=', 'array-contains', 'in', 'array-contains-any').required(),
|
|
6
|
+
value: Joi.any().required()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const pageOptions = Joi.object({
|
|
10
|
+
limit: Joi.number().integer().min(1).max(10000).default(50),
|
|
11
|
+
offset: Joi.number().integer().min(0).default(0)
|
|
12
|
+
}).optional()
|
|
13
|
+
|
|
14
|
+
export const Schemas = {
|
|
15
|
+
listProjects: {
|
|
16
|
+
request: {
|
|
17
|
+
body: {}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
listCollections: {
|
|
22
|
+
request: {
|
|
23
|
+
body: {
|
|
24
|
+
projectId: Joi.string().optional()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
listDocuments: {
|
|
30
|
+
request: {
|
|
31
|
+
body: {
|
|
32
|
+
collectionPath: Joi.string().required(),
|
|
33
|
+
projectId: Joi.string().optional(),
|
|
34
|
+
page: pageOptions
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
getDocument: {
|
|
40
|
+
request: {
|
|
41
|
+
body: {
|
|
42
|
+
collectionPath: Joi.string().required(),
|
|
43
|
+
documentId: Joi.string().required(),
|
|
44
|
+
projectId: Joi.string().optional()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
createDocument: {
|
|
50
|
+
request: {
|
|
51
|
+
body: {
|
|
52
|
+
collectionPath: Joi.string().required(),
|
|
53
|
+
documentId: Joi.string().optional(),
|
|
54
|
+
fields: Joi.object().required(),
|
|
55
|
+
projectId: Joi.string().optional()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
setDocument: {
|
|
61
|
+
request: {
|
|
62
|
+
body: {
|
|
63
|
+
collectionPath: Joi.string().required(),
|
|
64
|
+
documentId: Joi.string().required(),
|
|
65
|
+
fields: Joi.object().required(),
|
|
66
|
+
projectId: Joi.string().optional()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
updateDocument: {
|
|
72
|
+
request: {
|
|
73
|
+
body: {
|
|
74
|
+
collectionPath: Joi.string().required(),
|
|
75
|
+
documentId: Joi.string().required(),
|
|
76
|
+
fields: Joi.object().required(),
|
|
77
|
+
projectId: Joi.string().optional()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
deleteDocument: {
|
|
83
|
+
request: {
|
|
84
|
+
body: {
|
|
85
|
+
collectionPath: Joi.string().required(),
|
|
86
|
+
documentId: Joi.string().required(),
|
|
87
|
+
projectId: Joi.string().optional()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
createBatch: {
|
|
93
|
+
request: {
|
|
94
|
+
body: {
|
|
95
|
+
collectionPath: Joi.string().required(),
|
|
96
|
+
documents: Joi.array().items(
|
|
97
|
+
Joi.object({
|
|
98
|
+
documentId: Joi.string().optional(),
|
|
99
|
+
fields: Joi.object().required()
|
|
100
|
+
})
|
|
101
|
+
).min(1).max(500).required(),
|
|
102
|
+
projectId: Joi.string().optional()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
deleteByQuery: {
|
|
108
|
+
request: {
|
|
109
|
+
body: {
|
|
110
|
+
collectionPath: Joi.string().required(),
|
|
111
|
+
where: whereClause.required(),
|
|
112
|
+
projectId: Joi.string().optional()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
deleteByPrefix: {
|
|
118
|
+
request: {
|
|
119
|
+
body: {
|
|
120
|
+
collectionPath: Joi.string().required(),
|
|
121
|
+
prefix: Joi.string().required(),
|
|
122
|
+
projectId: Joi.string().optional()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
deleteBatch: {
|
|
128
|
+
request: {
|
|
129
|
+
body: {
|
|
130
|
+
collectionPath: Joi.string().required(),
|
|
131
|
+
documentIds: Joi.array().items(Joi.string()).min(1).max(500).required(),
|
|
132
|
+
projectId: Joi.string().optional()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
executeQuery: {
|
|
138
|
+
request: {
|
|
139
|
+
body: {
|
|
140
|
+
collectionPath: Joi.string().required(),
|
|
141
|
+
where: Joi.array().items(whereClause).optional(),
|
|
142
|
+
orderBy: Joi.object({
|
|
143
|
+
field: Joi.string().required(),
|
|
144
|
+
direction: Joi.string().valid('ASCENDING', 'DESCENDING').default('ASCENDING')
|
|
145
|
+
}).optional(),
|
|
146
|
+
limit: Joi.number().integer().min(1).max(10000).default(50),
|
|
147
|
+
projectId: Joi.string().optional()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
clearCollection: {
|
|
153
|
+
request: {
|
|
154
|
+
body: {
|
|
155
|
+
collectionPath: Joi.string().required(),
|
|
156
|
+
projectId: Joi.string().optional()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/mcp/tools/data.js
CHANGED
|
@@ -5,6 +5,7 @@ export function registerDataTools (server, apiClient) {
|
|
|
5
5
|
'platform_stats',
|
|
6
6
|
'Get platform dashboard statistics (topic count, message count, log entries, MQTT clients, etc.)',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/dashboard/stats')
|
|
@@ -19,6 +20,7 @@ export function registerDataTools (server, apiClient) {
|
|
|
19
20
|
'platform_export',
|
|
20
21
|
'Export all platform data (topics, subscriptions, messages, logs, etc.)',
|
|
21
22
|
{},
|
|
23
|
+
{ readOnlyHint: true },
|
|
22
24
|
async () => {
|
|
23
25
|
try {
|
|
24
26
|
const data = await apiClient.post('/v1/data/export')
|
|
@@ -31,11 +33,12 @@ export function registerDataTools (server, apiClient) {
|
|
|
31
33
|
|
|
32
34
|
server.tool(
|
|
33
35
|
'platform_import',
|
|
34
|
-
'Import platform data (optionally clearing existing data first)',
|
|
36
|
+
'[DESTRUCTIVE] Import platform data (optionally clearing existing data first). Ask the user for confirmation before calling this tool.',
|
|
35
37
|
{
|
|
36
38
|
data: z.record(z.any()).describe('Platform data object to import'),
|
|
37
39
|
clearExisting: z.boolean().optional().describe('Whether to clear existing data before importing')
|
|
38
40
|
},
|
|
41
|
+
{ destructiveHint: true },
|
|
39
42
|
async ({ data, clearExisting }) => {
|
|
40
43
|
try {
|
|
41
44
|
const body = { data }
|
|
@@ -50,8 +53,9 @@ export function registerDataTools (server, apiClient) {
|
|
|
50
53
|
|
|
51
54
|
server.tool(
|
|
52
55
|
'platform_clear',
|
|
53
|
-
'Clear all platform data (topics, messages, logs, traffic records)',
|
|
56
|
+
'[DESTRUCTIVE] Clear all platform data (topics, messages, logs, traffic records). Ask the user for confirmation before calling this tool.',
|
|
54
57
|
{},
|
|
58
|
+
{ destructiveHint: true },
|
|
55
59
|
async () => {
|
|
56
60
|
try {
|
|
57
61
|
const data = await apiClient.post('/v1/data/clear')
|
|
@@ -64,11 +68,12 @@ export function registerDataTools (server, apiClient) {
|
|
|
64
68
|
|
|
65
69
|
server.tool(
|
|
66
70
|
'platform_clear_services',
|
|
67
|
-
'Clear data for specific services. Useful for resetting test state without clearing everything.',
|
|
71
|
+
'[DESTRUCTIVE] Clear data for specific services. Useful for resetting test state without clearing everything. Ask the user for confirmation before calling this tool.',
|
|
68
72
|
{
|
|
69
73
|
services: z.array(z.string()).optional().describe('Optional list of service names to clear'),
|
|
70
74
|
keepSystemData: z.boolean().optional().describe('Whether to keep system/infrastructure data')
|
|
71
75
|
},
|
|
76
|
+
{ destructiveHint: true },
|
|
72
77
|
async ({ services, keepSystemData }) => {
|
|
73
78
|
try {
|
|
74
79
|
const body = {}
|
package/src/mcp/tools/docker.js
CHANGED
|
@@ -63,6 +63,7 @@ export function registerDockerTools (server, apiClient) {
|
|
|
63
63
|
'docker_list_containers',
|
|
64
64
|
'List all Docker containers on the goki-network with their status, ports, and uptime',
|
|
65
65
|
{},
|
|
66
|
+
{ readOnlyHint: true },
|
|
66
67
|
async () => {
|
|
67
68
|
try {
|
|
68
69
|
const { stdout } = await execAsync(
|
|
@@ -114,10 +115,11 @@ export function registerDockerTools (server, apiClient) {
|
|
|
114
115
|
|
|
115
116
|
server.tool(
|
|
116
117
|
'docker_stop_container',
|
|
117
|
-
'Stop a running Docker container by name',
|
|
118
|
+
'[DESTRUCTIVE] Stop a running Docker container by name. Ask the user for confirmation before calling this tool.',
|
|
118
119
|
{
|
|
119
120
|
containerName: z.string().describe('Name of the Docker container to stop')
|
|
120
121
|
},
|
|
122
|
+
{ destructiveHint: true },
|
|
121
123
|
async ({ containerName }) => {
|
|
122
124
|
try {
|
|
123
125
|
await execAsync(`docker stop ${containerName}`)
|
|
@@ -131,10 +133,11 @@ export function registerDockerTools (server, apiClient) {
|
|
|
131
133
|
|
|
132
134
|
server.tool(
|
|
133
135
|
'docker_restart_container',
|
|
134
|
-
'Restart a Docker container by name',
|
|
136
|
+
'[DESTRUCTIVE] Restart a Docker container by name. Ask the user for confirmation before calling this tool.',
|
|
135
137
|
{
|
|
136
138
|
containerName: z.string().describe('Name of the Docker container to restart')
|
|
137
139
|
},
|
|
140
|
+
{ destructiveHint: true },
|
|
138
141
|
async ({ containerName }) => {
|
|
139
142
|
try {
|
|
140
143
|
await execAsync(`docker restart ${containerName}`)
|
|
@@ -153,6 +156,7 @@ export function registerDockerTools (server, apiClient) {
|
|
|
153
156
|
containerName: z.string().describe('Name of the Docker container'),
|
|
154
157
|
lines: z.number().optional().describe('Number of recent log lines to retrieve (default 100)')
|
|
155
158
|
},
|
|
159
|
+
{ readOnlyHint: true },
|
|
156
160
|
async ({ containerName, lines = 100 }) => {
|
|
157
161
|
try {
|
|
158
162
|
const { stdout, stderr } = await execAsync(`docker logs --tail ${lines} ${containerName} 2>&1`)
|
|
@@ -5,6 +5,7 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
5
5
|
'firestore_list_projects',
|
|
6
6
|
'List configured Firestore emulator projects',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/firestore/projects/list')
|
|
@@ -21,6 +22,7 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
21
22
|
{
|
|
22
23
|
projectId: z.string().optional().describe('Firestore project ID')
|
|
23
24
|
},
|
|
25
|
+
{ readOnlyHint: true },
|
|
24
26
|
async ({ projectId }) => {
|
|
25
27
|
try {
|
|
26
28
|
const data = await apiClient.post('/v1/firestore/collections/list', { projectId })
|
|
@@ -39,6 +41,7 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
39
41
|
collectionPath: z.string().describe('Collection path'),
|
|
40
42
|
page: z.object({ limit: z.number().optional(), offset: z.number().optional() }).optional().describe('Pagination options with limit and offset')
|
|
41
43
|
},
|
|
44
|
+
{ readOnlyHint: true },
|
|
42
45
|
async ({ projectId, collectionPath, page }) => {
|
|
43
46
|
try {
|
|
44
47
|
const data = await apiClient.post('/v1/firestore/documents/list', { projectId, collectionPath, page })
|
|
@@ -57,6 +60,7 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
57
60
|
collectionPath: z.string().describe('Collection path'),
|
|
58
61
|
documentId: z.string().describe('Document ID')
|
|
59
62
|
},
|
|
63
|
+
{ readOnlyHint: true },
|
|
60
64
|
async ({ projectId, collectionPath, documentId }) => {
|
|
61
65
|
try {
|
|
62
66
|
const data = await apiClient.post('/v1/firestore/documents/get', { projectId, collectionPath, documentId })
|
|
@@ -107,12 +111,13 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
107
111
|
|
|
108
112
|
server.tool(
|
|
109
113
|
'firestore_delete_document',
|
|
110
|
-
'Delete a Firestore document',
|
|
114
|
+
'[DESTRUCTIVE] Delete a Firestore document permanently. Ask the user for confirmation before calling this tool.',
|
|
111
115
|
{
|
|
112
116
|
projectId: z.string().optional().describe('Firestore project ID'),
|
|
113
117
|
collectionPath: z.string().describe('Collection path'),
|
|
114
118
|
documentId: z.string().describe('Document ID')
|
|
115
119
|
},
|
|
120
|
+
{ destructiveHint: true },
|
|
116
121
|
async ({ projectId, collectionPath, documentId }) => {
|
|
117
122
|
try {
|
|
118
123
|
const data = await apiClient.post('/v1/firestore/documents/delete', { projectId, collectionPath, documentId })
|
|
@@ -123,6 +128,46 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
123
128
|
}
|
|
124
129
|
)
|
|
125
130
|
|
|
131
|
+
server.tool(
|
|
132
|
+
'firestore_set_document',
|
|
133
|
+
'Set (create or replace) a Firestore document by ID',
|
|
134
|
+
{
|
|
135
|
+
projectId: z.string().optional().describe('Firestore project ID'),
|
|
136
|
+
collectionPath: z.string().describe('Collection path'),
|
|
137
|
+
documentId: z.string().describe('Document ID'),
|
|
138
|
+
fields: z.record(z.any()).describe('Document fields in Firestore format')
|
|
139
|
+
},
|
|
140
|
+
async ({ projectId, collectionPath, documentId, fields }) => {
|
|
141
|
+
try {
|
|
142
|
+
const data = await apiClient.post('/v1/firestore/documents/set', { projectId, collectionPath, documentId, fields })
|
|
143
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
server.tool(
|
|
151
|
+
'firestore_create_batch',
|
|
152
|
+
'Create multiple Firestore documents in a collection at once',
|
|
153
|
+
{
|
|
154
|
+
projectId: z.string().optional().describe('Firestore project ID'),
|
|
155
|
+
collectionPath: z.string().describe('Collection path'),
|
|
156
|
+
documents: z.array(z.object({
|
|
157
|
+
documentId: z.string().optional().describe('Document ID (auto-generated if omitted)'),
|
|
158
|
+
fields: z.record(z.any()).describe('Document fields in Firestore format')
|
|
159
|
+
})).describe('Array of documents to create')
|
|
160
|
+
},
|
|
161
|
+
async ({ projectId, collectionPath, documents }) => {
|
|
162
|
+
try {
|
|
163
|
+
const data = await apiClient.post('/v1/firestore/documents/create-batch', { projectId, collectionPath, documents })
|
|
164
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
126
171
|
server.tool(
|
|
127
172
|
'firestore_query',
|
|
128
173
|
'Execute a query against a Firestore collection with optional where/orderBy/limit',
|
|
@@ -133,6 +178,7 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
133
178
|
orderBy: z.array(z.any()).optional().describe('Array of orderBy clauses'),
|
|
134
179
|
limit: z.number().optional().describe('Maximum number of results')
|
|
135
180
|
},
|
|
181
|
+
{ readOnlyHint: true },
|
|
136
182
|
async ({ projectId, collectionPath, where, orderBy, limit }) => {
|
|
137
183
|
try {
|
|
138
184
|
const data = await apiClient.post('/v1/firestore/query/execute', { projectId, collectionPath, where, orderBy, limit })
|
|
@@ -145,11 +191,12 @@ export function registerFirestoreTools (server, apiClient) {
|
|
|
145
191
|
|
|
146
192
|
server.tool(
|
|
147
193
|
'firestore_clear_collection',
|
|
148
|
-
'Delete
|
|
194
|
+
'[DESTRUCTIVE] Delete ALL documents in a Firestore collection. This cannot be undone. Ask the user for confirmation before calling this tool.',
|
|
149
195
|
{
|
|
150
196
|
projectId: z.string().optional().describe('Firestore project ID'),
|
|
151
197
|
collectionPath: z.string().describe('Collection path')
|
|
152
198
|
},
|
|
199
|
+
{ destructiveHint: true },
|
|
153
200
|
async ({ projectId, collectionPath }) => {
|
|
154
201
|
try {
|
|
155
202
|
const data = await apiClient.post('/v1/firestore/collection/clear', { projectId, collectionPath })
|
|
@@ -45,6 +45,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
45
45
|
limit: z.number().optional().describe('Maximum number of results'),
|
|
46
46
|
offset: z.number().optional().describe('Offset for pagination')
|
|
47
47
|
},
|
|
48
|
+
{ readOnlyHint: true },
|
|
48
49
|
async ({ triggerType, status, limit, offset }) => {
|
|
49
50
|
try {
|
|
50
51
|
const body = {}
|
|
@@ -69,6 +70,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
69
70
|
id: z.string().optional().describe('Function ID'),
|
|
70
71
|
name: z.string().optional().describe('Function name')
|
|
71
72
|
},
|
|
73
|
+
{ readOnlyHint: true },
|
|
72
74
|
async ({ id, name }) => {
|
|
73
75
|
try {
|
|
74
76
|
const body = {}
|
|
@@ -118,11 +120,12 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
118
120
|
|
|
119
121
|
server.tool(
|
|
120
122
|
'functions_delete',
|
|
121
|
-
'Delete a Cloud Function from the dev-tools emulator',
|
|
123
|
+
'[DESTRUCTIVE] Delete a Cloud Function from the dev-tools emulator. Ask the user for confirmation before calling this tool.',
|
|
122
124
|
{
|
|
123
125
|
id: z.string().optional().describe('Function ID'),
|
|
124
126
|
name: z.string().optional().describe('Function name')
|
|
125
127
|
},
|
|
128
|
+
{ destructiveHint: true },
|
|
126
129
|
async ({ id, name }) => {
|
|
127
130
|
try {
|
|
128
131
|
const body = {}
|
|
@@ -158,11 +161,12 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
158
161
|
|
|
159
162
|
server.tool(
|
|
160
163
|
'functions_stop',
|
|
161
|
-
'Stop a running Cloud Function',
|
|
164
|
+
'[DESTRUCTIVE] Stop a running Cloud Function. Ask the user for confirmation before calling this tool.',
|
|
162
165
|
{
|
|
163
166
|
id: z.string().optional().describe('Function ID'),
|
|
164
167
|
name: z.string().optional().describe('Function name')
|
|
165
168
|
},
|
|
169
|
+
{ destructiveHint: true },
|
|
166
170
|
async ({ id, name }) => {
|
|
167
171
|
try {
|
|
168
172
|
const body = {}
|
|
@@ -178,11 +182,12 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
178
182
|
|
|
179
183
|
server.tool(
|
|
180
184
|
'functions_restart',
|
|
181
|
-
'Restart a Cloud Function (stop then start)',
|
|
185
|
+
'[DESTRUCTIVE] Restart a Cloud Function (stop then start). Ask the user for confirmation before calling this tool.',
|
|
182
186
|
{
|
|
183
187
|
id: z.string().optional().describe('Function ID'),
|
|
184
188
|
name: z.string().optional().describe('Function name')
|
|
185
189
|
},
|
|
190
|
+
{ destructiveHint: true },
|
|
186
191
|
async ({ id, name }) => {
|
|
187
192
|
try {
|
|
188
193
|
const body = {}
|
|
@@ -228,6 +233,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
228
233
|
limit: z.number().optional().describe('Maximum number of results (default: 50)'),
|
|
229
234
|
offset: z.number().optional().describe('Offset for pagination')
|
|
230
235
|
},
|
|
236
|
+
{ readOnlyHint: true },
|
|
231
237
|
async ({ functionId, functionName, triggerType, limit, offset }) => {
|
|
232
238
|
try {
|
|
233
239
|
const body = {}
|
|
@@ -248,6 +254,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
248
254
|
'functions_stats',
|
|
249
255
|
'Get aggregate statistics for Cloud Functions (counts by trigger type, status, invocations)',
|
|
250
256
|
{},
|
|
257
|
+
{ readOnlyHint: true },
|
|
251
258
|
async () => {
|
|
252
259
|
try {
|
|
253
260
|
const data = await apiClient.post('/v1/functions/stats')
|
|
@@ -264,6 +271,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
264
271
|
{
|
|
265
272
|
name: z.string().describe('Function name')
|
|
266
273
|
},
|
|
274
|
+
{ readOnlyHint: true },
|
|
267
275
|
async ({ name }) => {
|
|
268
276
|
try {
|
|
269
277
|
const data = await apiClient.post('/v1/functions/dependencies/read', { name })
|
|
@@ -301,6 +309,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
301
309
|
id: z.string().optional().describe('Function ID'),
|
|
302
310
|
name: z.string().optional().describe('Function name')
|
|
303
311
|
},
|
|
312
|
+
{ readOnlyHint: true },
|
|
304
313
|
async ({ id, name }) => {
|
|
305
314
|
try {
|
|
306
315
|
const body = {}
|
|
@@ -322,6 +331,7 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
322
331
|
name: z.string().optional().describe('Function name'),
|
|
323
332
|
filePath: z.string().describe('Relative file path within the function source directory')
|
|
324
333
|
},
|
|
334
|
+
{ readOnlyHint: true },
|
|
325
335
|
async ({ id, name, filePath }) => {
|
|
326
336
|
try {
|
|
327
337
|
const body = { filePath }
|
|
@@ -359,12 +369,13 @@ export function registerFunctionsTools (server, apiClient) {
|
|
|
359
369
|
|
|
360
370
|
server.tool(
|
|
361
371
|
'functions_files_delete',
|
|
362
|
-
'Delete a file from a Cloud Function source directory',
|
|
372
|
+
'[DESTRUCTIVE] Delete a file from a Cloud Function source directory. Ask the user for confirmation before calling this tool.',
|
|
363
373
|
{
|
|
364
374
|
id: z.string().optional().describe('Function ID'),
|
|
365
375
|
name: z.string().optional().describe('Function name'),
|
|
366
376
|
filePath: z.string().describe('Relative file path within the function source directory')
|
|
367
377
|
},
|
|
378
|
+
{ destructiveHint: true },
|
|
368
379
|
async ({ id, name, filePath }) => {
|
|
369
380
|
try {
|
|
370
381
|
const body = { filePath }
|
|
@@ -9,6 +9,7 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
9
9
|
limit: z.number().optional().describe('Maximum number of records to return'),
|
|
10
10
|
offset: z.number().optional().describe('Number of records to skip for pagination')
|
|
11
11
|
},
|
|
12
|
+
{ readOnlyHint: true },
|
|
12
13
|
async ({ filter, limit, offset }) => {
|
|
13
14
|
try {
|
|
14
15
|
const body = {}
|
|
@@ -29,6 +30,7 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
29
30
|
{
|
|
30
31
|
id: z.string().describe('ID of the HTTP traffic entry to retrieve')
|
|
31
32
|
},
|
|
33
|
+
{ readOnlyHint: true },
|
|
32
34
|
async ({ id }) => {
|
|
33
35
|
try {
|
|
34
36
|
const data = await apiClient.post('/v1/http-traffic/details', { id })
|
|
@@ -46,6 +48,7 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
46
48
|
filter: z.record(z.any()).describe('Required filter criteria (method, targetHost, sourceService, statusCode, traceId, pathContains)'),
|
|
47
49
|
timeout: z.number().optional().describe('Timeout in ms (default: 5000, max: 30000)')
|
|
48
50
|
},
|
|
51
|
+
{ readOnlyHint: true },
|
|
49
52
|
async ({ filter, timeout }) => {
|
|
50
53
|
try {
|
|
51
54
|
const body = { filter }
|
|
@@ -60,8 +63,9 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
60
63
|
|
|
61
64
|
server.tool(
|
|
62
65
|
'http_traffic_clear',
|
|
63
|
-
'Clear all recorded HTTP traffic',
|
|
66
|
+
'[DESTRUCTIVE] Clear all recorded HTTP traffic. Ask the user for confirmation before calling this tool.',
|
|
64
67
|
{},
|
|
68
|
+
{ destructiveHint: true },
|
|
65
69
|
async () => {
|
|
66
70
|
try {
|
|
67
71
|
const data = await apiClient.post('/v1/http-traffic/clear')
|
|
@@ -76,6 +80,7 @@ export function registerHttpTrafficTools (server, apiClient) {
|
|
|
76
80
|
'http_traffic_stats',
|
|
77
81
|
'Get HTTP traffic statistics grouped by host, method, and error rates',
|
|
78
82
|
{},
|
|
83
|
+
{ readOnlyHint: true },
|
|
79
84
|
async () => {
|
|
80
85
|
try {
|
|
81
86
|
const data = await apiClient.post('/v1/http-traffic/stats')
|
package/src/mcp/tools/logging.js
CHANGED
|
@@ -5,6 +5,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
5
5
|
'logging_list_services',
|
|
6
6
|
'List all services that have sent log entries to the dev-tools logging emulator',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/logging/services/list')
|
|
@@ -29,6 +30,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
29
30
|
limit: z.number().optional().describe('Max entries to return (default: 100, max: 1000)'),
|
|
30
31
|
offset: z.number().optional().describe('Skip this many entries for pagination (default: 0)')
|
|
31
32
|
},
|
|
33
|
+
{ readOnlyHint: true },
|
|
32
34
|
async ({ serviceName, severity, textContains, traceId, sinceTimestamp, untilTimestamp, compact, limit, offset }) => {
|
|
33
35
|
try {
|
|
34
36
|
const filter = {}
|
|
@@ -61,6 +63,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
61
63
|
{
|
|
62
64
|
ids: z.array(z.string()).describe('Array of log entry IDs to retrieve (from the _id field in compact list results)')
|
|
63
65
|
},
|
|
66
|
+
{ readOnlyHint: true },
|
|
64
67
|
async ({ ids }) => {
|
|
65
68
|
try {
|
|
66
69
|
const data = await apiClient.post('/v1/logging/entries/get', { ids })
|
|
@@ -73,8 +76,9 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
73
76
|
|
|
74
77
|
server.tool(
|
|
75
78
|
'logging_clear',
|
|
76
|
-
'Clear all log entries from the dev-tools logging emulator',
|
|
79
|
+
'[DESTRUCTIVE] Clear all log entries from the dev-tools logging emulator. Ask the user for confirmation before calling this tool.',
|
|
77
80
|
{},
|
|
81
|
+
{ destructiveHint: true },
|
|
78
82
|
async () => {
|
|
79
83
|
try {
|
|
80
84
|
const data = await apiClient.post('/v1/logging/entries/clear')
|
|
@@ -91,6 +95,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
91
95
|
{
|
|
92
96
|
filter: z.record(z.any()).optional().describe('Optional filter criteria to scope the export')
|
|
93
97
|
},
|
|
98
|
+
{ readOnlyHint: true },
|
|
94
99
|
async ({ filter }) => {
|
|
95
100
|
try {
|
|
96
101
|
const body = {}
|
|
@@ -109,6 +114,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
109
114
|
{
|
|
110
115
|
traceId: z.string().describe('The trace ID to look up')
|
|
111
116
|
},
|
|
117
|
+
{ readOnlyHint: true },
|
|
112
118
|
async ({ traceId }) => {
|
|
113
119
|
try {
|
|
114
120
|
const data = await apiClient.post(`/v1/logging/trace/${traceId}`)
|
|
@@ -126,6 +132,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
126
132
|
filter: z.record(z.any()).describe('Filter criteria the log entry must match'),
|
|
127
133
|
timeout: z.number().optional().describe('Maximum seconds to wait before timing out')
|
|
128
134
|
},
|
|
135
|
+
{ readOnlyHint: true },
|
|
129
136
|
async ({ filter, timeout }) => {
|
|
130
137
|
try {
|
|
131
138
|
const body = { filter }
|
|
@@ -145,6 +152,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
145
152
|
traceId: z.string().optional().describe('Optional trace ID to scope the assertion to'),
|
|
146
153
|
sinceTimestamp: z.string().optional().describe('Optional ISO timestamp — only check logs after this time')
|
|
147
154
|
},
|
|
155
|
+
{ readOnlyHint: true },
|
|
148
156
|
async ({ traceId, sinceTimestamp }) => {
|
|
149
157
|
try {
|
|
150
158
|
const body = {}
|
|
@@ -162,6 +170,7 @@ export function registerLoggingTools (server, apiClient) {
|
|
|
162
170
|
'logging_get_queue_stats',
|
|
163
171
|
'Get Bull queue statistics including job counts and health status for the blackbox logs consumer',
|
|
164
172
|
{},
|
|
173
|
+
{ readOnlyHint: true },
|
|
165
174
|
async () => {
|
|
166
175
|
try {
|
|
167
176
|
const data = await apiClient.post('/v1/logging/queue-stats', {})
|
package/src/mcp/tools/mqtt.js
CHANGED
|
@@ -5,6 +5,7 @@ export function registerMqttTools (server, apiClient) {
|
|
|
5
5
|
'mqtt_list_clients',
|
|
6
6
|
'List all connected MQTT clients with their connection info',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/mqtt/clients/list')
|
|
@@ -22,6 +23,7 @@ export function registerMqttTools (server, apiClient) {
|
|
|
22
23
|
filter: z.record(z.any()).optional().describe('Optional filter criteria for MQTT messages'),
|
|
23
24
|
page: z.object({ limit: z.number().optional(), offset: z.number().optional() }).optional().describe('Optional pagination with limit and offset')
|
|
24
25
|
},
|
|
26
|
+
{ readOnlyHint: true },
|
|
25
27
|
async ({ filter, page }) => {
|
|
26
28
|
try {
|
|
27
29
|
const body = {}
|
|
@@ -5,6 +5,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
5
5
|
'postgres_test_connection',
|
|
6
6
|
'Test the PostgreSQL database connection',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/postgres/connection/test')
|
|
@@ -19,6 +20,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
19
20
|
'postgres_list_databases',
|
|
20
21
|
'List all PostgreSQL databases',
|
|
21
22
|
{},
|
|
23
|
+
{ readOnlyHint: true },
|
|
22
24
|
async () => {
|
|
23
25
|
try {
|
|
24
26
|
const data = await apiClient.post('/v1/postgres/databases/list')
|
|
@@ -33,6 +35,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
33
35
|
'postgres_list_schemas',
|
|
34
36
|
'List all schemas in the current database',
|
|
35
37
|
{},
|
|
38
|
+
{ readOnlyHint: true },
|
|
36
39
|
async () => {
|
|
37
40
|
try {
|
|
38
41
|
const data = await apiClient.post('/v1/postgres/schemas/list')
|
|
@@ -49,6 +52,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
49
52
|
{
|
|
50
53
|
schema: z.string().optional().describe('Schema name (defaults to public)')
|
|
51
54
|
},
|
|
55
|
+
{ readOnlyHint: true },
|
|
52
56
|
async ({ schema }) => {
|
|
53
57
|
try {
|
|
54
58
|
const data = await apiClient.post('/v1/postgres/tables/list', { schema })
|
|
@@ -66,6 +70,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
66
70
|
table: z.string().describe('Table name'),
|
|
67
71
|
schema: z.string().optional().describe('Schema name (defaults to public)')
|
|
68
72
|
},
|
|
73
|
+
{ readOnlyHint: true },
|
|
69
74
|
async ({ table, schema }) => {
|
|
70
75
|
try {
|
|
71
76
|
const data = await apiClient.post('/v1/postgres/columns/list', { table, schema })
|
|
@@ -83,6 +88,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
83
88
|
table: z.string().describe('Table name'),
|
|
84
89
|
page: z.object({ limit: z.number().optional(), offset: z.number().optional() }).optional().describe('Pagination options with limit and offset')
|
|
85
90
|
},
|
|
91
|
+
{ readOnlyHint: true },
|
|
86
92
|
async ({ table, page }) => {
|
|
87
93
|
try {
|
|
88
94
|
const data = await apiClient.post('/v1/postgres/rows/list', { table, page })
|
|
@@ -95,11 +101,12 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
95
101
|
|
|
96
102
|
server.tool(
|
|
97
103
|
'postgres_execute_query',
|
|
98
|
-
'Execute a raw SQL query against PostgreSQL',
|
|
104
|
+
'[DESTRUCTIVE] Execute a raw SQL query against PostgreSQL. Ask the user for confirmation before calling this tool.',
|
|
99
105
|
{
|
|
100
106
|
query: z.string().describe('SQL query to execute'),
|
|
101
107
|
params: z.array(z.any()).optional().describe('Query parameters for parameterized queries')
|
|
102
108
|
},
|
|
109
|
+
{ destructiveHint: true },
|
|
103
110
|
async ({ query, params }) => {
|
|
104
111
|
try {
|
|
105
112
|
const data = await apiClient.post('/v1/postgres/query/execute', { query, params })
|
|
@@ -118,6 +125,7 @@ export function registerPostgresTools (server, apiClient) {
|
|
|
118
125
|
condition: z.record(z.any()).optional().describe('Condition to match against query results'),
|
|
119
126
|
timeout: z.number().optional().describe('Timeout in milliseconds')
|
|
120
127
|
},
|
|
128
|
+
{ readOnlyHint: true },
|
|
121
129
|
async ({ query, condition, timeout }) => {
|
|
122
130
|
try {
|
|
123
131
|
const data = await apiClient.post('/v1/postgres/rows/wait-for', { query, condition, timeout })
|
package/src/mcp/tools/pubsub.js
CHANGED
|
@@ -5,6 +5,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
5
5
|
'pubsub_list_topics',
|
|
6
6
|
'List all Pub/Sub topics in the dev-tools emulator',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/pubsub/topics/list')
|
|
@@ -33,10 +34,11 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
33
34
|
|
|
34
35
|
server.tool(
|
|
35
36
|
'pubsub_delete_topic',
|
|
36
|
-
'Delete a Pub/Sub topic from the dev-tools emulator',
|
|
37
|
+
'[DESTRUCTIVE] Delete a Pub/Sub topic from the dev-tools emulator. Ask the user for confirmation before calling this tool.',
|
|
37
38
|
{
|
|
38
39
|
topicName: z.string().describe('Name of the topic to delete')
|
|
39
40
|
},
|
|
41
|
+
{ destructiveHint: true },
|
|
40
42
|
async ({ topicName }) => {
|
|
41
43
|
try {
|
|
42
44
|
const data = await apiClient.post('/v1/pubsub/topics/delete', { topicName })
|
|
@@ -54,6 +56,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
54
56
|
query: z.string().describe('Search query to match against topic names'),
|
|
55
57
|
limit: z.number().optional().describe('Maximum number of suggestions to return')
|
|
56
58
|
},
|
|
59
|
+
{ readOnlyHint: true },
|
|
57
60
|
async ({ query, limit }) => {
|
|
58
61
|
try {
|
|
59
62
|
const body = { query }
|
|
@@ -70,6 +73,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
70
73
|
'pubsub_list_subscriptions',
|
|
71
74
|
'List all Pub/Sub subscriptions in the dev-tools emulator',
|
|
72
75
|
{},
|
|
76
|
+
{ readOnlyHint: true },
|
|
73
77
|
async () => {
|
|
74
78
|
try {
|
|
75
79
|
const data = await apiClient.post('/v1/pubsub/subscriptions/list')
|
|
@@ -99,10 +103,11 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
99
103
|
|
|
100
104
|
server.tool(
|
|
101
105
|
'pubsub_delete_subscription',
|
|
102
|
-
'Delete a Pub/Sub subscription from the dev-tools emulator',
|
|
106
|
+
'[DESTRUCTIVE] Delete a Pub/Sub subscription from the dev-tools emulator. Ask the user for confirmation before calling this tool.',
|
|
103
107
|
{
|
|
104
108
|
subscriptionName: z.string().describe('Name of the subscription to delete')
|
|
105
109
|
},
|
|
110
|
+
{ destructiveHint: true },
|
|
106
111
|
async ({ subscriptionName }) => {
|
|
107
112
|
try {
|
|
108
113
|
const data = await apiClient.post('/v1/pubsub/subscriptions/delete', { subscriptionName })
|
|
@@ -140,6 +145,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
140
145
|
subscriptionName: z.string().describe('Subscription to pull messages from'),
|
|
141
146
|
maxMessages: z.number().optional().describe('Maximum number of messages to pull')
|
|
142
147
|
},
|
|
148
|
+
{ readOnlyHint: true },
|
|
143
149
|
async ({ subscriptionName, maxMessages }) => {
|
|
144
150
|
try {
|
|
145
151
|
const body = { subscriptionName }
|
|
@@ -160,6 +166,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
160
166
|
searchIn: z.array(z.string()).optional().describe('Optional list of fields to search in (e.g. ["data", "attributes", "topic"])'),
|
|
161
167
|
filter: z.record(z.any()).optional().describe('Optional additional filter criteria')
|
|
162
168
|
},
|
|
169
|
+
{ readOnlyHint: true },
|
|
163
170
|
async ({ query, searchIn, filter }) => {
|
|
164
171
|
try {
|
|
165
172
|
const body = { query }
|
|
@@ -180,6 +187,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
180
187
|
filter: z.record(z.any()).optional().describe('Optional filter criteria (e.g. by topic, time range)'),
|
|
181
188
|
page: z.object({ limit: z.number().optional(), offset: z.number().optional() }).optional().describe('Optional pagination with limit and offset')
|
|
182
189
|
},
|
|
190
|
+
{ readOnlyHint: true },
|
|
183
191
|
async ({ filter, page }) => {
|
|
184
192
|
try {
|
|
185
193
|
const body = {}
|
|
@@ -195,11 +203,12 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
195
203
|
|
|
196
204
|
server.tool(
|
|
197
205
|
'pubsub_clear_history',
|
|
198
|
-
'Clear Pub/Sub message history. Optionally keep a number of recent messages or clear only a specific topic.',
|
|
206
|
+
'[DESTRUCTIVE] Clear Pub/Sub message history. Optionally keep a number of recent messages or clear only a specific topic. Ask the user for confirmation before calling this tool.',
|
|
199
207
|
{
|
|
200
208
|
keepCount: z.number().optional().describe('Number of recent messages to keep'),
|
|
201
209
|
topic: z.string().optional().describe('Only clear history for this topic')
|
|
202
210
|
},
|
|
211
|
+
{ destructiveHint: true },
|
|
203
212
|
async ({ keepCount, topic }) => {
|
|
204
213
|
try {
|
|
205
214
|
const body = {}
|
|
@@ -217,6 +226,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
217
226
|
'pubsub_history_stats',
|
|
218
227
|
'Get statistics about Pub/Sub message history including counts per topic and total volume',
|
|
219
228
|
{},
|
|
229
|
+
{ readOnlyHint: true },
|
|
220
230
|
async () => {
|
|
221
231
|
try {
|
|
222
232
|
const data = await apiClient.post('/v1/pubsub/history/stats')
|
|
@@ -234,6 +244,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
234
244
|
filter: z.record(z.any()).describe('Filter criteria with jsonPath and/or predicate to match incoming messages'),
|
|
235
245
|
timeout: z.number().optional().describe('Maximum seconds to wait before timing out')
|
|
236
246
|
},
|
|
247
|
+
{ readOnlyHint: true },
|
|
237
248
|
async ({ filter, timeout }) => {
|
|
238
249
|
try {
|
|
239
250
|
const body = { filter }
|
|
@@ -252,6 +263,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
252
263
|
{
|
|
253
264
|
filter: z.record(z.any()).describe('Filter criteria: { topic (string), since (ISO timestamp), dataContains (string), attributesMatch (object) }')
|
|
254
265
|
},
|
|
266
|
+
{ readOnlyHint: true },
|
|
255
267
|
async ({ filter }) => {
|
|
256
268
|
try {
|
|
257
269
|
const data = await apiClient.post('/v1/pubsub/messages/assert-published', { filter })
|
|
@@ -285,6 +297,7 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
285
297
|
{
|
|
286
298
|
projectName: z.string().optional().describe('Optional project name to filter by')
|
|
287
299
|
},
|
|
300
|
+
{ readOnlyHint: true },
|
|
288
301
|
async ({ projectName }) => {
|
|
289
302
|
try {
|
|
290
303
|
const body = {}
|
|
@@ -299,11 +312,12 @@ export function registerPubsubTools (server, apiClient) {
|
|
|
299
312
|
|
|
300
313
|
server.tool(
|
|
301
314
|
'pubsub_registry_unregister',
|
|
302
|
-
'Unregister topics for a project from the persistent Pub/Sub registry',
|
|
315
|
+
'[DESTRUCTIVE] Unregister topics for a project from the persistent Pub/Sub registry. Ask the user for confirmation before calling this tool.',
|
|
303
316
|
{
|
|
304
317
|
projectName: z.string().describe('Project name to unregister topics from'),
|
|
305
318
|
topicNames: z.array(z.string()).describe('Array of topic name strings to unregister')
|
|
306
319
|
},
|
|
320
|
+
{ destructiveHint: true },
|
|
307
321
|
async ({ projectName, topicNames }) => {
|
|
308
322
|
try {
|
|
309
323
|
const data = await apiClient.post('/v1/pubsub/registry/unregister', { projectName, topicNames })
|
package/src/mcp/tools/redis.js
CHANGED
|
@@ -5,6 +5,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
5
5
|
'redis_test_connection',
|
|
6
6
|
'Test the Redis connection',
|
|
7
7
|
{},
|
|
8
|
+
{ readOnlyHint: true },
|
|
8
9
|
async () => {
|
|
9
10
|
try {
|
|
10
11
|
const data = await apiClient.post('/v1/redis/connection/test')
|
|
@@ -19,6 +20,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
19
20
|
'redis_info',
|
|
20
21
|
'Get Redis server info including memory usage and db size',
|
|
21
22
|
{},
|
|
23
|
+
{ readOnlyHint: true },
|
|
22
24
|
async () => {
|
|
23
25
|
try {
|
|
24
26
|
const data = await apiClient.post('/v1/redis/info')
|
|
@@ -37,6 +39,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
37
39
|
cursor: z.string().optional().describe('Cursor for pagination'),
|
|
38
40
|
count: z.number().optional().describe('Approximate number of keys to return per scan')
|
|
39
41
|
},
|
|
42
|
+
{ readOnlyHint: true },
|
|
40
43
|
async ({ pattern, cursor, count }) => {
|
|
41
44
|
try {
|
|
42
45
|
const data = await apiClient.post('/v1/redis/keys/scan', { pattern, cursor, count })
|
|
@@ -53,6 +56,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
53
56
|
{
|
|
54
57
|
key: z.string().describe('Redis key name')
|
|
55
58
|
},
|
|
59
|
+
{ readOnlyHint: true },
|
|
56
60
|
async ({ key }) => {
|
|
57
61
|
try {
|
|
58
62
|
const data = await apiClient.post('/v1/redis/keys/get', { key })
|
|
@@ -69,6 +73,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
69
73
|
{
|
|
70
74
|
key: z.string().describe('Redis key name')
|
|
71
75
|
},
|
|
76
|
+
{ readOnlyHint: true },
|
|
72
77
|
async ({ key }) => {
|
|
73
78
|
try {
|
|
74
79
|
const data = await apiClient.post('/v1/redis/keys/ttl', { key })
|
|
@@ -81,10 +86,11 @@ export function registerRedisTools (server, apiClient) {
|
|
|
81
86
|
|
|
82
87
|
server.tool(
|
|
83
88
|
'redis_delete_key',
|
|
84
|
-
'Delete a specific Redis key',
|
|
89
|
+
'[DESTRUCTIVE] Delete a specific Redis key. Ask the user for confirmation before calling this tool.',
|
|
85
90
|
{
|
|
86
91
|
key: z.string().describe('Redis key name')
|
|
87
92
|
},
|
|
93
|
+
{ destructiveHint: true },
|
|
88
94
|
async ({ key }) => {
|
|
89
95
|
try {
|
|
90
96
|
const data = await apiClient.post('/v1/redis/keys/delete', { key })
|
|
@@ -115,8 +121,9 @@ export function registerRedisTools (server, apiClient) {
|
|
|
115
121
|
|
|
116
122
|
server.tool(
|
|
117
123
|
'redis_delete_all',
|
|
118
|
-
'Delete all Redis keys (FLUSHDB)',
|
|
124
|
+
'[DESTRUCTIVE] Delete all Redis keys (FLUSHDB). Ask the user for confirmation before calling this tool.',
|
|
119
125
|
{},
|
|
126
|
+
{ destructiveHint: true },
|
|
120
127
|
async () => {
|
|
121
128
|
try {
|
|
122
129
|
const data = await apiClient.post('/v1/redis/keys/delete-all')
|
|
@@ -134,6 +141,7 @@ export function registerRedisTools (server, apiClient) {
|
|
|
134
141
|
condition: z.record(z.any()).describe('Condition to match against the key value'),
|
|
135
142
|
timeout: z.number().optional().describe('Timeout in milliseconds')
|
|
136
143
|
},
|
|
144
|
+
{ readOnlyHint: true },
|
|
137
145
|
async ({ condition, timeout }) => {
|
|
138
146
|
try {
|
|
139
147
|
const data = await apiClient.post('/v1/redis/keys/wait-for', { condition, timeout })
|
|
@@ -3,6 +3,7 @@ export function registerServicesTools (server, apiClient) {
|
|
|
3
3
|
'services_list',
|
|
4
4
|
'List all dev-tools platform services and their status',
|
|
5
5
|
{},
|
|
6
|
+
{ readOnlyHint: true },
|
|
6
7
|
async () => {
|
|
7
8
|
try {
|
|
8
9
|
const data = await apiClient.post('/v1/services/list')
|
|
@@ -17,6 +18,7 @@ export function registerServicesTools (server, apiClient) {
|
|
|
17
18
|
'services_status',
|
|
18
19
|
'Get detailed status of all services',
|
|
19
20
|
{},
|
|
21
|
+
{ readOnlyHint: true },
|
|
20
22
|
async () => {
|
|
21
23
|
try {
|
|
22
24
|
const data = await apiClient.post('/v1/services/status')
|
|
@@ -29,8 +31,9 @@ export function registerServicesTools (server, apiClient) {
|
|
|
29
31
|
|
|
30
32
|
server.tool(
|
|
31
33
|
'services_restart',
|
|
32
|
-
'Restart dev-tools services',
|
|
34
|
+
'[DESTRUCTIVE] Restart dev-tools services. Ask the user for confirmation before calling this tool.',
|
|
33
35
|
{},
|
|
36
|
+
{ destructiveHint: true },
|
|
34
37
|
async () => {
|
|
35
38
|
try {
|
|
36
39
|
const data = await apiClient.post('/v1/services/restart')
|
|
@@ -57,8 +60,9 @@ export function registerServicesTools (server, apiClient) {
|
|
|
57
60
|
|
|
58
61
|
server.tool(
|
|
59
62
|
'scheduler_stop',
|
|
60
|
-
'Stop the scheduler tick',
|
|
63
|
+
'[DESTRUCTIVE] Stop the scheduler tick. Ask the user for confirmation before calling this tool.',
|
|
61
64
|
{},
|
|
65
|
+
{ destructiveHint: true },
|
|
62
66
|
async () => {
|
|
63
67
|
try {
|
|
64
68
|
const data = await apiClient.post('/v1/scheduler/tick/stop')
|
|
@@ -73,6 +77,7 @@ export function registerServicesTools (server, apiClient) {
|
|
|
73
77
|
'scheduler_status',
|
|
74
78
|
'Get scheduler tick status (running/stopped, interval)',
|
|
75
79
|
{},
|
|
80
|
+
{ readOnlyHint: true },
|
|
76
81
|
async () => {
|
|
77
82
|
try {
|
|
78
83
|
const data = await apiClient.post('/v1/scheduler/tick/status')
|
|
@@ -113,8 +118,9 @@ export function registerServicesTools (server, apiClient) {
|
|
|
113
118
|
|
|
114
119
|
server.tool(
|
|
115
120
|
'gateway_stop',
|
|
116
|
-
'Stop the app gateway service',
|
|
121
|
+
'[DESTRUCTIVE] Stop the app gateway service. Ask the user for confirmation before calling this tool.',
|
|
117
122
|
{},
|
|
123
|
+
{ destructiveHint: true },
|
|
118
124
|
async () => {
|
|
119
125
|
try {
|
|
120
126
|
const data = await apiClient.post('/v1/gateway/stop')
|
|
@@ -129,6 +135,7 @@ export function registerServicesTools (server, apiClient) {
|
|
|
129
135
|
'gateway_status',
|
|
130
136
|
'Get the app gateway status',
|
|
131
137
|
{},
|
|
138
|
+
{ readOnlyHint: true },
|
|
132
139
|
async () => {
|
|
133
140
|
try {
|
|
134
141
|
const data = await apiClient.post('/v1/gateway/status')
|
|
@@ -157,6 +164,7 @@ export function registerServicesTools (server, apiClient) {
|
|
|
157
164
|
'gateway_health_check',
|
|
158
165
|
'Run health checks on all gateway endpoints',
|
|
159
166
|
{},
|
|
167
|
+
{ readOnlyHint: true },
|
|
160
168
|
async () => {
|
|
161
169
|
try {
|
|
162
170
|
const data = await apiClient.post('/v1/gateway/healthCheck')
|
|
@@ -21,11 +21,12 @@ export function registerSnapshotTools (server, apiClient) {
|
|
|
21
21
|
|
|
22
22
|
server.tool(
|
|
23
23
|
'snapshots_restore',
|
|
24
|
-
'Restore development environment from a snapshot. This will stop microservices, restore data, and restart them.',
|
|
24
|
+
'[DESTRUCTIVE] Restore development environment from a snapshot. This will stop microservices, restore data, and restart them. Ask the user for confirmation before calling this tool.',
|
|
25
25
|
{
|
|
26
26
|
snapshotId: z.string().describe('Snapshot ID to restore from (e.g., "e2e_2026-02-26_10-30-45")'),
|
|
27
27
|
restartServices: z.boolean().optional().describe('Whether to restart microservices after restore (default: true)')
|
|
28
28
|
},
|
|
29
|
+
{ destructiveHint: true },
|
|
29
30
|
async ({ snapshotId, restartServices }) => {
|
|
30
31
|
try {
|
|
31
32
|
const body = { snapshotId }
|
|
@@ -42,6 +43,7 @@ export function registerSnapshotTools (server, apiClient) {
|
|
|
42
43
|
'snapshots_list',
|
|
43
44
|
'List all available snapshots with their metadata',
|
|
44
45
|
{},
|
|
46
|
+
{ readOnlyHint: true },
|
|
45
47
|
async () => {
|
|
46
48
|
try {
|
|
47
49
|
const data = await apiClient.post('/v1/snapshots/list', {})
|
|
@@ -58,6 +60,7 @@ export function registerSnapshotTools (server, apiClient) {
|
|
|
58
60
|
{
|
|
59
61
|
snapshotId: z.string().describe('Snapshot ID to get details for')
|
|
60
62
|
},
|
|
63
|
+
{ readOnlyHint: true },
|
|
61
64
|
async ({ snapshotId }) => {
|
|
62
65
|
try {
|
|
63
66
|
const data = await apiClient.post('/v1/snapshots/details', { snapshotId })
|
|
@@ -70,10 +73,11 @@ export function registerSnapshotTools (server, apiClient) {
|
|
|
70
73
|
|
|
71
74
|
server.tool(
|
|
72
75
|
'snapshots_delete',
|
|
73
|
-
'Delete a snapshot and its files',
|
|
76
|
+
'[DESTRUCTIVE] Delete a snapshot and its files. Ask the user for confirmation before calling this tool.',
|
|
74
77
|
{
|
|
75
78
|
snapshotId: z.string().optional().describe('Snapshot ID to delete. If omitted, deletes all snapshots.')
|
|
76
79
|
},
|
|
80
|
+
{ destructiveHint: true },
|
|
77
81
|
async ({ snapshotId }) => {
|
|
78
82
|
try {
|
|
79
83
|
const body = {}
|
|
@@ -27,6 +27,7 @@ export function registerWebhooksTools (server, apiClient) {
|
|
|
27
27
|
'webhooks_list',
|
|
28
28
|
'List all registered webhooks',
|
|
29
29
|
{},
|
|
30
|
+
{ readOnlyHint: true },
|
|
30
31
|
async () => {
|
|
31
32
|
try {
|
|
32
33
|
const data = await apiClient.post('/v1/webhooks/list')
|
|
@@ -39,10 +40,11 @@ export function registerWebhooksTools (server, apiClient) {
|
|
|
39
40
|
|
|
40
41
|
server.tool(
|
|
41
42
|
'webhooks_remove',
|
|
42
|
-
'Remove a registered webhook by its prefix',
|
|
43
|
+
'[DESTRUCTIVE] Remove a registered webhook by its prefix. Ask the user for confirmation before calling this tool.',
|
|
43
44
|
{
|
|
44
45
|
prefix: z.string().describe('URL prefix of the webhook to remove')
|
|
45
46
|
},
|
|
47
|
+
{ destructiveHint: true },
|
|
46
48
|
async ({ prefix }) => {
|
|
47
49
|
try {
|
|
48
50
|
const data = await apiClient.post('/v1/webhooks/remove', { prefix })
|
|
@@ -73,6 +75,7 @@ export function registerWebhooksTools (server, apiClient) {
|
|
|
73
75
|
'webhooks_tunnel_status',
|
|
74
76
|
'Get ngrok tunnel status (connected/disconnected, public URL)',
|
|
75
77
|
{},
|
|
78
|
+
{ readOnlyHint: true },
|
|
76
79
|
async () => {
|
|
77
80
|
try {
|
|
78
81
|
const data = await apiClient.post('/v1/webhooks/tunnel/status')
|
|
@@ -87,6 +90,7 @@ export function registerWebhooksTools (server, apiClient) {
|
|
|
87
90
|
'webhooks_settings_get',
|
|
88
91
|
'Get current webhook/ngrok settings',
|
|
89
92
|
{},
|
|
93
|
+
{ readOnlyHint: true },
|
|
90
94
|
async () => {
|
|
91
95
|
try {
|
|
92
96
|
const data = await apiClient.post('/v1/webhooks/settings/get')
|