@jaypie/mcp 0.3.4 → 0.4.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/createMcpServer.d.ts +7 -1
- package/dist/index.js +22 -2088
- package/dist/index.js.map +1 -1
- package/dist/suite.js +1197 -7
- package/dist/suite.js.map +1 -1
- package/package.json +1 -2
- package/release-notes/fabric/0.1.3.md +25 -0
- package/release-notes/fabric/0.1.4.md +42 -0
- package/release-notes/mcp/0.4.0.md +27 -0
- package/release-notes/testkit/1.2.15.md +23 -0
- package/skills/fabric.md +30 -3
- package/dist/aws-B3dW_-bD.js +0 -1202
- package/dist/aws-B3dW_-bD.js.map +0 -1
- package/prompts/Branch_Management.md +0 -34
- package/prompts/Development_Process.md +0 -89
- package/prompts/Jaypie_Agent_Rules.md +0 -110
- package/prompts/Jaypie_Auth0_Express_Mongoose.md +0 -736
- package/prompts/Jaypie_Browser_and_Frontend_Web_Packages.md +0 -18
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +0 -430
- package/prompts/Jaypie_CICD_with_GitHub_Actions.md +0 -371
- package/prompts/Jaypie_Commander_CLI_Package.md +0 -166
- package/prompts/Jaypie_Core_Errors_and_Logging.md +0 -39
- package/prompts/Jaypie_DynamoDB_Package.md +0 -774
- package/prompts/Jaypie_Eslint_NPM_Package.md +0 -78
- package/prompts/Jaypie_Express_Package.md +0 -630
- package/prompts/Jaypie_Fabric_Commander.md +0 -411
- package/prompts/Jaypie_Fabric_LLM.md +0 -312
- package/prompts/Jaypie_Fabric_Lambda.md +0 -308
- package/prompts/Jaypie_Fabric_MCP.md +0 -316
- package/prompts/Jaypie_Fabric_Package.md +0 -599
- package/prompts/Jaypie_Fabricator.md +0 -617
- package/prompts/Jaypie_Ideal_Project_Structure.md +0 -78
- package/prompts/Jaypie_Init_CICD_with_GitHub_Actions.md +0 -1186
- package/prompts/Jaypie_Init_Express_on_Lambda.md +0 -115
- package/prompts/Jaypie_Init_Jaypie_CDK_Package.md +0 -35
- package/prompts/Jaypie_Init_Lambda_Package.md +0 -505
- package/prompts/Jaypie_Init_Monorepo_Project.md +0 -44
- package/prompts/Jaypie_Init_Project_Subpackage.md +0 -65
- package/prompts/Jaypie_Legacy_Patterns.md +0 -15
- package/prompts/Jaypie_Llm_Calls.md +0 -449
- package/prompts/Jaypie_Llm_Tools.md +0 -155
- package/prompts/Jaypie_MCP_Package.md +0 -281
- package/prompts/Jaypie_Mocks_and_Testkit.md +0 -137
- package/prompts/Jaypie_Repokit.md +0 -103
- package/prompts/Jaypie_Scrub.md +0 -177
- package/prompts/Jaypie_Streaming.md +0 -467
- package/prompts/Templates_CDK_Subpackage.md +0 -115
- package/prompts/Templates_Express_Subpackage.md +0 -187
- package/prompts/Templates_Project_Monorepo.md +0 -326
- package/prompts/Templates_Project_Subpackage.md +0 -93
- package/prompts/Write_Efficient_Prompt_Guides.md +0 -48
- package/prompts/Write_and_Maintain_Engaging_Readme.md +0 -67
package/dist/index.js
CHANGED
|
@@ -1,183 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
6
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import * as fs from 'node:fs/promises';
|
|
10
|
-
import matter from 'gray-matter';
|
|
11
|
-
import { gt } from 'semver';
|
|
12
|
-
import { g as getDatadogCredentials, s as searchDatadogLogs, a as aggregateDatadogLogs, l as listDatadogMonitors, b as getDatadogSyntheticResults, c as listDatadogSynthetics, q as queryDatadogMetrics, d as searchDatadogRum, e as debugLlmCall, f as listLlmProviders, h as listAwsProfiles, i as listStepFunctionExecutions, j as stopStepFunctionExecution, k as listLambdaFunctions, m as getLambdaFunction, n as filterLogEvents, o as listS3Objects, p as describeStack, r as describeDynamoDBTable, t as scanDynamoDB, u as queryDynamoDB, v as getDynamoDBItem, w as listSQSQueues, x as getSQSQueueAttributes, y as receiveSQSMessage, z as purgeSQSQueue } from './aws-B3dW_-bD.js';
|
|
6
|
+
import { createMcpServerFromSuite } from '@jaypie/fabric/mcp';
|
|
7
|
+
import { suite } from './suite.js';
|
|
13
8
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
14
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import '@jaypie/fabric';
|
|
11
|
+
import 'node:fs/promises';
|
|
12
|
+
import 'gray-matter';
|
|
13
|
+
import 'semver';
|
|
15
14
|
import 'node:https';
|
|
16
15
|
import '@jaypie/llm';
|
|
17
16
|
import 'node:child_process';
|
|
18
17
|
import 'node:os';
|
|
19
18
|
|
|
20
|
-
const BUILD_VERSION_STRING = "@jaypie/mcp@0.3.4#a6510094"
|
|
21
|
-
;
|
|
22
|
-
const __filename$1 = fileURLToPath(import.meta.url);
|
|
23
|
-
const __dirname$1 = path.dirname(__filename$1);
|
|
24
|
-
const PROMPTS_PATH = path.join(__dirname$1, "..", "prompts");
|
|
25
|
-
const RELEASE_NOTES_PATH = path.join(__dirname$1, "..", "release-notes");
|
|
26
|
-
const SKILLS_PATH = path.join(__dirname$1, "..", "skills");
|
|
27
|
-
// Logger utility
|
|
28
|
-
function createLogger(verbose) {
|
|
29
|
-
return {
|
|
30
|
-
info: (message, ...args) => {
|
|
31
|
-
if (verbose) {
|
|
32
|
-
console.error(`[jaypie-mcp] ${message}`, ...args);
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
error: (message, ...args) => {
|
|
36
|
-
console.error(`[jaypie-mcp ERROR] ${message}`, ...args);
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
async function parseMarkdownFile(filePath) {
|
|
41
|
-
try {
|
|
42
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
43
|
-
const filename = path.basename(filePath);
|
|
44
|
-
if (content.startsWith("---")) {
|
|
45
|
-
const parsed = matter(content);
|
|
46
|
-
const frontMatter = parsed.data;
|
|
47
|
-
return {
|
|
48
|
-
filename,
|
|
49
|
-
description: frontMatter.description,
|
|
50
|
-
include: frontMatter.include || frontMatter.globs,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return { filename };
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return { filename: path.basename(filePath) };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function formatPromptListItem(prompt) {
|
|
60
|
-
const { filename, description, include } = prompt;
|
|
61
|
-
if (description && include) {
|
|
62
|
-
return `* ${filename}: ${description} - Required for ${include}`;
|
|
63
|
-
}
|
|
64
|
-
else if (description) {
|
|
65
|
-
return `* ${filename}: ${description}`;
|
|
66
|
-
}
|
|
67
|
-
else if (include) {
|
|
68
|
-
return `* ${filename} - Required for ${include}`;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
return `* ${filename}`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function parseReleaseNoteFile(filePath) {
|
|
75
|
-
try {
|
|
76
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
77
|
-
const filename = path.basename(filePath, ".md");
|
|
78
|
-
if (content.startsWith("---")) {
|
|
79
|
-
const parsed = matter(content);
|
|
80
|
-
const frontMatter = parsed.data;
|
|
81
|
-
return {
|
|
82
|
-
date: frontMatter.date,
|
|
83
|
-
filename,
|
|
84
|
-
summary: frontMatter.summary,
|
|
85
|
-
version: frontMatter.version || filename,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
return { filename, version: filename };
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return { filename: path.basename(filePath, ".md") };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function formatReleaseNoteListItem(note) {
|
|
95
|
-
const { date, packageName, summary, version } = note;
|
|
96
|
-
const parts = [`* ${packageName}@${version}`];
|
|
97
|
-
if (date) {
|
|
98
|
-
parts.push(`(${date})`);
|
|
99
|
-
}
|
|
100
|
-
if (summary) {
|
|
101
|
-
parts.push(`- ${summary}`);
|
|
102
|
-
}
|
|
103
|
-
return parts.join(" ");
|
|
104
|
-
}
|
|
105
|
-
async function getPackageReleaseNotes(packageName) {
|
|
106
|
-
const packageDir = path.join(RELEASE_NOTES_PATH, packageName);
|
|
107
|
-
try {
|
|
108
|
-
const files = await fs.readdir(packageDir);
|
|
109
|
-
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
110
|
-
const notes = await Promise.all(mdFiles.map(async (file) => {
|
|
111
|
-
const parsed = await parseReleaseNoteFile(path.join(packageDir, file));
|
|
112
|
-
return { ...parsed, packageName };
|
|
113
|
-
}));
|
|
114
|
-
// Sort by version descending (newest first)
|
|
115
|
-
return notes.sort((a, b) => {
|
|
116
|
-
if (!a.version || !b.version)
|
|
117
|
-
return 0;
|
|
118
|
-
try {
|
|
119
|
-
return gt(a.version, b.version) ? -1 : 1;
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// If semver comparison fails, fall back to string comparison
|
|
123
|
-
return b.version.localeCompare(a.version);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function filterReleaseNotesSince(notes, sinceVersion) {
|
|
132
|
-
return notes.filter((note) => {
|
|
133
|
-
if (!note.version)
|
|
134
|
-
return false;
|
|
135
|
-
try {
|
|
136
|
-
return gt(note.version, sinceVersion);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
function isValidSkillAlias(alias) {
|
|
144
|
-
const normalized = alias.toLowerCase().trim();
|
|
145
|
-
// Reject if contains path separators or traversal
|
|
146
|
-
if (normalized.includes("/") ||
|
|
147
|
-
normalized.includes("\\") ||
|
|
148
|
-
normalized.includes("..")) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
// Only allow alphanumeric, hyphens, underscores
|
|
152
|
-
return /^[a-z0-9_-]+$/.test(normalized);
|
|
153
|
-
}
|
|
154
|
-
async function parseSkillFile(filePath) {
|
|
155
|
-
try {
|
|
156
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
157
|
-
const alias = path.basename(filePath, ".md");
|
|
158
|
-
if (content.startsWith("---")) {
|
|
159
|
-
const parsed = matter(content);
|
|
160
|
-
const frontMatter = parsed.data;
|
|
161
|
-
return {
|
|
162
|
-
alias,
|
|
163
|
-
description: frontMatter.description,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
return { alias };
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
return { alias: path.basename(filePath, ".md") };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
function formatSkillListItem(skill) {
|
|
173
|
-
const { alias, description } = skill;
|
|
174
|
-
if (description) {
|
|
175
|
-
return `* ${alias} - ${description}`;
|
|
176
|
-
}
|
|
177
|
-
return `* ${alias}`;
|
|
178
|
-
}
|
|
179
19
|
/**
|
|
180
20
|
* Creates and configures an MCP server instance with Jaypie tools
|
|
21
|
+
*
|
|
22
|
+
* Uses ServiceSuite to register all services as MCP tools automatically.
|
|
23
|
+
* Services are defined in suite.ts using fabricService and registered
|
|
24
|
+
* by category. The createMcpServerFromSuite bridge converts them to
|
|
25
|
+
* MCP tools with proper Zod schema validation.
|
|
26
|
+
*
|
|
181
27
|
* @param options - Configuration options (or legacy version string)
|
|
182
28
|
* @returns Configured MCP server instance
|
|
183
29
|
*/
|
|
@@ -185,1929 +31,17 @@ function createMcpServer(options = {}) {
|
|
|
185
31
|
// Support legacy signature: createMcpServer(version: string)
|
|
186
32
|
const config = typeof options === "string" ? { version: options } : options;
|
|
187
33
|
const { version = "0.0.0", verbose = false } = config;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const server =
|
|
192
|
-
name:
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.error("[jaypie-mcp] Creating MCP server instance from suite");
|
|
36
|
+
}
|
|
37
|
+
const server = createMcpServerFromSuite(suite, {
|
|
38
|
+
name: suite.name,
|
|
193
39
|
version,
|
|
194
|
-
}, {
|
|
195
|
-
capabilities: {},
|
|
196
|
-
});
|
|
197
|
-
log.info("Registering tools...");
|
|
198
|
-
server.tool("list_prompts", "[DEPRECATED: Use skill('index') instead] List available Jaypie development prompts and guides. Use this FIRST when starting work on a Jaypie project to discover relevant documentation. Returns filenames, descriptions, and which file patterns each prompt applies to (e.g., 'Required for packages/express/**').", {}, async () => {
|
|
199
|
-
log.info("Tool called: list_prompts");
|
|
200
|
-
log.info(`Reading directory: ${PROMPTS_PATH}`);
|
|
201
|
-
try {
|
|
202
|
-
const files = await fs.readdir(PROMPTS_PATH);
|
|
203
|
-
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
204
|
-
log.info(`Found ${mdFiles.length} .md files`);
|
|
205
|
-
const prompts = await Promise.all(mdFiles.map((file) => parseMarkdownFile(path.join(PROMPTS_PATH, file))));
|
|
206
|
-
const formattedList = prompts.map(formatPromptListItem).join("\n");
|
|
207
|
-
log.info("Successfully listed prompts");
|
|
208
|
-
return {
|
|
209
|
-
content: [
|
|
210
|
-
{
|
|
211
|
-
type: "text",
|
|
212
|
-
text: formattedList || "No .md files found in the prompts directory.",
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
log.error("Error listing prompts:", error);
|
|
219
|
-
return {
|
|
220
|
-
content: [
|
|
221
|
-
{
|
|
222
|
-
type: "text",
|
|
223
|
-
text: `Error listing prompts: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
log.info("Registered tool: list_prompts");
|
|
230
|
-
server.tool("read_prompt", "[DEPRECATED: Use skill(alias) instead] Read a Jaypie prompt/guide by filename. Call list_prompts first to see available prompts. These contain best practices, templates, code patterns, and step-by-step guides for Jaypie development tasks.", {
|
|
231
|
-
filename: z
|
|
232
|
-
.string()
|
|
233
|
-
.describe("The prompt filename from list_prompts (e.g., 'Jaypie_Express_Package.md', 'Development_Process.md')"),
|
|
234
|
-
}, async ({ filename }) => {
|
|
235
|
-
log.info(`Tool called: read_prompt (filename: ${filename})`);
|
|
236
|
-
try {
|
|
237
|
-
const filePath = path.join(PROMPTS_PATH, filename);
|
|
238
|
-
log.info(`Reading file: ${filePath}`);
|
|
239
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
240
|
-
log.info(`Successfully read ${filename} (${content.length} bytes)`);
|
|
241
|
-
return {
|
|
242
|
-
content: [
|
|
243
|
-
{
|
|
244
|
-
type: "text",
|
|
245
|
-
text: content,
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
if (error.code === "ENOENT") {
|
|
252
|
-
log.error(`File not found: ${filename}`);
|
|
253
|
-
return {
|
|
254
|
-
content: [
|
|
255
|
-
{
|
|
256
|
-
type: "text",
|
|
257
|
-
text: `Error: Prompt file "${filename}" not found in prompts directory`,
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
log.error("Error reading prompt file:", error);
|
|
263
|
-
return {
|
|
264
|
-
content: [
|
|
265
|
-
{
|
|
266
|
-
type: "text",
|
|
267
|
-
text: `Error reading prompt file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
log.info("Registered tool: read_prompt");
|
|
274
|
-
// Skill tool - new unified documentation access
|
|
275
|
-
server.tool("skill", "Access Jaypie development documentation. Pass a skill alias (e.g., 'aws', 'tests', 'errors') to get that documentation. Pass 'index' or no argument to list all available skills.", {
|
|
276
|
-
alias: z
|
|
277
|
-
.string()
|
|
278
|
-
.optional()
|
|
279
|
-
.describe("Skill alias (e.g., 'aws', 'tests', 'errors'). Omit or use 'index' to list all skills."),
|
|
280
|
-
}, async ({ alias }) => {
|
|
281
|
-
const effectiveAlias = alias?.toLowerCase().trim() || "index";
|
|
282
|
-
log.info(`Tool called: skill (alias: ${effectiveAlias})`);
|
|
283
|
-
// Validate alias for path traversal
|
|
284
|
-
if (!isValidSkillAlias(effectiveAlias)) {
|
|
285
|
-
log.error(`Invalid skill alias: ${effectiveAlias}`);
|
|
286
|
-
return {
|
|
287
|
-
content: [
|
|
288
|
-
{
|
|
289
|
-
type: "text",
|
|
290
|
-
text: `Error: Invalid skill alias "${alias}". Aliases may only contain letters, numbers, hyphens, and underscores.`,
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
// Handle index: return index.md content plus list of all skills
|
|
297
|
-
if (effectiveAlias === "index") {
|
|
298
|
-
let indexContent = "";
|
|
299
|
-
// Try to read index.md if it exists
|
|
300
|
-
try {
|
|
301
|
-
const indexPath = path.join(SKILLS_PATH, "index.md");
|
|
302
|
-
const rawContent = await fs.readFile(indexPath, "utf-8");
|
|
303
|
-
// Strip frontmatter if present
|
|
304
|
-
if (rawContent.startsWith("---")) {
|
|
305
|
-
const parsed = matter(rawContent);
|
|
306
|
-
indexContent = parsed.content.trim();
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
indexContent = rawContent;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
// No index.md, that's fine
|
|
314
|
-
}
|
|
315
|
-
// Get list of all skills
|
|
316
|
-
const files = await fs.readdir(SKILLS_PATH);
|
|
317
|
-
const mdFiles = files.filter((file) => file.endsWith(".md") && file !== "index.md");
|
|
318
|
-
const skills = await Promise.all(mdFiles.map((file) => parseSkillFile(path.join(SKILLS_PATH, file))));
|
|
319
|
-
// Sort alphabetically
|
|
320
|
-
skills.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
321
|
-
const skillList = skills.map(formatSkillListItem).join("\n");
|
|
322
|
-
const resultText = indexContent
|
|
323
|
-
? `${indexContent}\n\n## Available Skills\n\n${skillList}`
|
|
324
|
-
: `# Jaypie Skills\n\n## Available Skills\n\n${skillList}`;
|
|
325
|
-
log.info("Successfully returned skill index");
|
|
326
|
-
return {
|
|
327
|
-
content: [
|
|
328
|
-
{
|
|
329
|
-
type: "text",
|
|
330
|
-
text: resultText,
|
|
331
|
-
},
|
|
332
|
-
],
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
// Read specific skill file
|
|
336
|
-
const skillPath = path.join(SKILLS_PATH, `${effectiveAlias}.md`);
|
|
337
|
-
log.info(`Reading skill file: ${skillPath}`);
|
|
338
|
-
const content = await fs.readFile(skillPath, "utf-8");
|
|
339
|
-
log.info(`Successfully read skill ${effectiveAlias} (${content.length} bytes)`);
|
|
340
|
-
return {
|
|
341
|
-
content: [
|
|
342
|
-
{
|
|
343
|
-
type: "text",
|
|
344
|
-
text: content,
|
|
345
|
-
},
|
|
346
|
-
],
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
if (error.code === "ENOENT") {
|
|
351
|
-
log.error(`Skill not found: ${effectiveAlias}`);
|
|
352
|
-
// Suggest available skills
|
|
353
|
-
try {
|
|
354
|
-
const files = await fs.readdir(SKILLS_PATH);
|
|
355
|
-
const available = files
|
|
356
|
-
.filter((f) => f.endsWith(".md"))
|
|
357
|
-
.map((f) => f.replace(".md", ""))
|
|
358
|
-
.sort()
|
|
359
|
-
.join(", ");
|
|
360
|
-
return {
|
|
361
|
-
content: [
|
|
362
|
-
{
|
|
363
|
-
type: "text",
|
|
364
|
-
text: `Error: Skill "${effectiveAlias}" not found.\n\nAvailable skills: ${available}\n\nUse skill("index") to see all skills with descriptions.`,
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
return {
|
|
371
|
-
content: [
|
|
372
|
-
{
|
|
373
|
-
type: "text",
|
|
374
|
-
text: `Error: Skill "${effectiveAlias}" not found. Use skill("index") to list available skills.`,
|
|
375
|
-
},
|
|
376
|
-
],
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
log.error("Error reading skill:", error);
|
|
381
|
-
return {
|
|
382
|
-
content: [
|
|
383
|
-
{
|
|
384
|
-
type: "text",
|
|
385
|
-
text: `Error reading skill: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
log.info("Registered tool: skill");
|
|
392
|
-
server.tool("version", `Prints the current version and hash, \`${BUILD_VERSION_STRING}\``, {}, async () => {
|
|
393
|
-
log.info("Tool called: version");
|
|
394
|
-
return {
|
|
395
|
-
content: [
|
|
396
|
-
{
|
|
397
|
-
type: "text",
|
|
398
|
-
text: BUILD_VERSION_STRING,
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
};
|
|
402
|
-
});
|
|
403
|
-
log.info("Registered tool: version");
|
|
404
|
-
// Release Notes Tools
|
|
405
|
-
server.tool("list_release_notes", "List available release notes for Jaypie packages. Filter by package name and/or get only versions newer than a specified version.", {
|
|
406
|
-
package: z
|
|
407
|
-
.string()
|
|
408
|
-
.optional()
|
|
409
|
-
.describe("Filter by package name (e.g., 'jaypie', 'mcp'). If not provided, lists release notes for all packages."),
|
|
410
|
-
since_version: z
|
|
411
|
-
.string()
|
|
412
|
-
.optional()
|
|
413
|
-
.describe("Only show versions newer than this (e.g., '1.0.0'). Uses semver comparison."),
|
|
414
|
-
}, async ({ package: packageFilter, since_version: sinceVersion }) => {
|
|
415
|
-
log.info("Tool called: list_release_notes");
|
|
416
|
-
log.info(`Release notes directory: ${RELEASE_NOTES_PATH}`);
|
|
417
|
-
try {
|
|
418
|
-
// Get list of package directories
|
|
419
|
-
const entries = await fs.readdir(RELEASE_NOTES_PATH, {
|
|
420
|
-
withFileTypes: true,
|
|
421
|
-
});
|
|
422
|
-
const packageDirs = entries
|
|
423
|
-
.filter((entry) => entry.isDirectory())
|
|
424
|
-
.map((entry) => entry.name);
|
|
425
|
-
log.info(`Found ${packageDirs.length} package directories`);
|
|
426
|
-
// Filter by package if specified
|
|
427
|
-
const packagesToList = packageFilter
|
|
428
|
-
? packageDirs.filter((pkg) => pkg === packageFilter)
|
|
429
|
-
: packageDirs;
|
|
430
|
-
if (packagesToList.length === 0 && packageFilter) {
|
|
431
|
-
return {
|
|
432
|
-
content: [
|
|
433
|
-
{
|
|
434
|
-
type: "text",
|
|
435
|
-
text: `No release notes found for package "${packageFilter}".`,
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
// Get release notes for each package
|
|
441
|
-
const allNotes = await Promise.all(packagesToList.map((pkg) => getPackageReleaseNotes(pkg)));
|
|
442
|
-
let flatNotes = allNotes.flat();
|
|
443
|
-
// Filter by since_version if specified
|
|
444
|
-
if (sinceVersion) {
|
|
445
|
-
flatNotes = filterReleaseNotesSince(flatNotes, sinceVersion);
|
|
446
|
-
}
|
|
447
|
-
if (flatNotes.length === 0) {
|
|
448
|
-
const filterDesc = sinceVersion ? ` newer than ${sinceVersion}` : "";
|
|
449
|
-
return {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: "text",
|
|
453
|
-
text: `No release notes found${filterDesc}.`,
|
|
454
|
-
},
|
|
455
|
-
],
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
const formattedList = flatNotes
|
|
459
|
-
.map(formatReleaseNoteListItem)
|
|
460
|
-
.join("\n");
|
|
461
|
-
log.info(`Successfully listed ${flatNotes.length} release notes`);
|
|
462
|
-
return {
|
|
463
|
-
content: [
|
|
464
|
-
{
|
|
465
|
-
type: "text",
|
|
466
|
-
text: formattedList,
|
|
467
|
-
},
|
|
468
|
-
],
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
log.error("Error listing release notes:", error);
|
|
473
|
-
return {
|
|
474
|
-
content: [
|
|
475
|
-
{
|
|
476
|
-
type: "text",
|
|
477
|
-
text: `Error listing release notes: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
478
|
-
},
|
|
479
|
-
],
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
40
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}, async ({ package: packageName, version }) => {
|
|
488
|
-
log.info(`Tool called: read_release_note (package: ${packageName}, version: ${version})`);
|
|
489
|
-
try {
|
|
490
|
-
const filePath = path.join(RELEASE_NOTES_PATH, packageName, `${version}.md`);
|
|
491
|
-
log.info(`Reading file: ${filePath}`);
|
|
492
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
493
|
-
log.info(`Successfully read release note for ${packageName}@${version} (${content.length} bytes)`);
|
|
494
|
-
return {
|
|
495
|
-
content: [
|
|
496
|
-
{
|
|
497
|
-
type: "text",
|
|
498
|
-
text: content,
|
|
499
|
-
},
|
|
500
|
-
],
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
if (error.code === "ENOENT") {
|
|
505
|
-
log.error(`Release note not found: ${packageName}@${version}`);
|
|
506
|
-
return {
|
|
507
|
-
content: [
|
|
508
|
-
{
|
|
509
|
-
type: "text",
|
|
510
|
-
text: `Error: Release note for "${packageName}@${version}" not found. Use list_release_notes to see available versions.`,
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
log.error("Error reading release note:", error);
|
|
516
|
-
return {
|
|
517
|
-
content: [
|
|
518
|
-
{
|
|
519
|
-
type: "text",
|
|
520
|
-
text: `Error reading release note: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
log.info("Registered tool: read_release_note");
|
|
527
|
-
// Datadog Logs Tool
|
|
528
|
-
server.tool("datadog_logs", "Search and retrieve individual Datadog log entries. Use this to view actual log messages and details. For aggregated counts/statistics (e.g., 'how many errors by service?'), use datadog_log_analytics instead. Requires DATADOG_API_KEY and DATADOG_APP_KEY environment variables.", {
|
|
529
|
-
query: z
|
|
530
|
-
.string()
|
|
531
|
-
.optional()
|
|
532
|
-
.describe("Search query to filter logs. Examples: 'status:error', '@http.status_code:500', '*timeout*', '@requestId:abc123'. Combined with DD_ENV, DD_SERVICE, DD_SOURCE env vars if set."),
|
|
533
|
-
source: z
|
|
534
|
-
.string()
|
|
535
|
-
.optional()
|
|
536
|
-
.describe("Override the log source (e.g., 'lambda', 'auth0', 'nginx'). If not provided, uses DD_SOURCE env var or defaults to 'lambda'."),
|
|
537
|
-
env: z
|
|
538
|
-
.string()
|
|
539
|
-
.optional()
|
|
540
|
-
.describe("Override the environment (e.g., 'sandbox', 'kitchen', 'lab', 'studio', 'production'). If not provided, uses DD_ENV env var."),
|
|
541
|
-
service: z
|
|
542
|
-
.string()
|
|
543
|
-
.optional()
|
|
544
|
-
.describe("Override the service name. If not provided, uses DD_SERVICE env var."),
|
|
545
|
-
from: z
|
|
546
|
-
.string()
|
|
547
|
-
.optional()
|
|
548
|
-
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
549
|
-
to: z
|
|
550
|
-
.string()
|
|
551
|
-
.optional()
|
|
552
|
-
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
553
|
-
limit: z
|
|
554
|
-
.number()
|
|
555
|
-
.optional()
|
|
556
|
-
.describe("Max logs to return (1-1000). Defaults to 50."),
|
|
557
|
-
sort: z
|
|
558
|
-
.enum(["timestamp", "-timestamp"])
|
|
559
|
-
.optional()
|
|
560
|
-
.describe("Sort order: 'timestamp' (oldest first) or '-timestamp' (newest first, default)."),
|
|
561
|
-
}, async ({ query, source, env, service, from, to, limit, sort }) => {
|
|
562
|
-
log.info("Tool called: datadog_logs");
|
|
563
|
-
const credentials = getDatadogCredentials();
|
|
564
|
-
if (!credentials) {
|
|
565
|
-
const missingApiKey = !process.env.DATADOG_API_KEY && !process.env.DD_API_KEY;
|
|
566
|
-
const missingAppKey = !process.env.DATADOG_APP_KEY &&
|
|
567
|
-
!process.env.DATADOG_APPLICATION_KEY &&
|
|
568
|
-
!process.env.DD_APP_KEY &&
|
|
569
|
-
!process.env.DD_APPLICATION_KEY;
|
|
570
|
-
if (missingApiKey) {
|
|
571
|
-
log.error("No Datadog API key found in environment");
|
|
572
|
-
return {
|
|
573
|
-
content: [
|
|
574
|
-
{
|
|
575
|
-
type: "text",
|
|
576
|
-
text: "Error: No Datadog API key found. Please set DATADOG_API_KEY or DD_API_KEY environment variable.",
|
|
577
|
-
},
|
|
578
|
-
],
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
if (missingAppKey) {
|
|
582
|
-
log.error("No Datadog Application key found in environment");
|
|
583
|
-
return {
|
|
584
|
-
content: [
|
|
585
|
-
{
|
|
586
|
-
type: "text",
|
|
587
|
-
text: "Error: No Datadog Application key found. Please set DATADOG_APP_KEY or DD_APP_KEY environment variable. The Logs Search API requires both an API key and an Application key.",
|
|
588
|
-
},
|
|
589
|
-
],
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
// credentials is guaranteed to be non-null here
|
|
594
|
-
const result = await searchDatadogLogs(credentials, {
|
|
595
|
-
query,
|
|
596
|
-
source,
|
|
597
|
-
env,
|
|
598
|
-
service,
|
|
599
|
-
from,
|
|
600
|
-
to,
|
|
601
|
-
limit,
|
|
602
|
-
sort,
|
|
603
|
-
}, log);
|
|
604
|
-
if (!result.success) {
|
|
605
|
-
return {
|
|
606
|
-
content: [
|
|
607
|
-
{
|
|
608
|
-
type: "text",
|
|
609
|
-
text: `Error from Datadog API: ${result.error}`,
|
|
610
|
-
},
|
|
611
|
-
],
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
if (result.logs.length === 0) {
|
|
615
|
-
return {
|
|
616
|
-
content: [
|
|
617
|
-
{
|
|
618
|
-
type: "text",
|
|
619
|
-
text: `No logs found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
620
|
-
},
|
|
621
|
-
],
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
const resultText = [
|
|
625
|
-
`Query: ${result.query}`,
|
|
626
|
-
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
627
|
-
`Found ${result.logs.length} log entries:`,
|
|
628
|
-
"",
|
|
629
|
-
JSON.stringify(result.logs, null, 2),
|
|
630
|
-
].join("\n");
|
|
631
|
-
return {
|
|
632
|
-
content: [
|
|
633
|
-
{
|
|
634
|
-
type: "text",
|
|
635
|
-
text: resultText,
|
|
636
|
-
},
|
|
637
|
-
],
|
|
638
|
-
};
|
|
639
|
-
});
|
|
640
|
-
log.info("Registered tool: datadog_logs");
|
|
641
|
-
// Datadog Log Analytics Tool
|
|
642
|
-
server.tool("datadog_log_analytics", "Aggregate and analyze Datadog logs by grouping them by fields. Use this for statistics and counts (e.g., 'errors by service', 'requests by status code'). For viewing individual log entries, use datadog_logs instead.", {
|
|
643
|
-
groupBy: z
|
|
644
|
-
.array(z.string())
|
|
645
|
-
.describe("Fields to group by. Examples: ['source'], ['service', 'status'], ['@http.status_code']. Common facets: source, service, status, host, @http.status_code, @env."),
|
|
646
|
-
query: z
|
|
647
|
-
.string()
|
|
648
|
-
.optional()
|
|
649
|
-
.describe("Filter query. Examples: 'status:error', '*timeout*', '@http.method:POST'. Use '*' for all logs."),
|
|
650
|
-
source: z
|
|
651
|
-
.string()
|
|
652
|
-
.optional()
|
|
653
|
-
.describe("Override the log source filter. Use '*' to include all sources. If not provided, uses DD_SOURCE env var or defaults to 'lambda'."),
|
|
654
|
-
env: z
|
|
655
|
-
.string()
|
|
656
|
-
.optional()
|
|
657
|
-
.describe("Override the environment filter. If not provided, uses DD_ENV env var."),
|
|
658
|
-
service: z
|
|
659
|
-
.string()
|
|
660
|
-
.optional()
|
|
661
|
-
.describe("Override the service name filter. If not provided, uses DD_SERVICE env var."),
|
|
662
|
-
from: z
|
|
663
|
-
.string()
|
|
664
|
-
.optional()
|
|
665
|
-
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
666
|
-
to: z
|
|
667
|
-
.string()
|
|
668
|
-
.optional()
|
|
669
|
-
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
670
|
-
aggregation: z
|
|
671
|
-
.enum(["count", "avg", "sum", "min", "max", "cardinality"])
|
|
672
|
-
.optional()
|
|
673
|
-
.describe("Aggregation type. 'count' counts logs, others require a metric field. Defaults to 'count'."),
|
|
674
|
-
metric: z
|
|
675
|
-
.string()
|
|
676
|
-
.optional()
|
|
677
|
-
.describe("Metric field to aggregate when using avg, sum, min, max, or cardinality. E.g., '@duration', '@http.response_time'."),
|
|
678
|
-
}, async ({ groupBy, query, source, env, service, from, to, aggregation, metric, }) => {
|
|
679
|
-
log.info("Tool called: datadog_log_analytics");
|
|
680
|
-
const credentials = getDatadogCredentials();
|
|
681
|
-
if (!credentials) {
|
|
682
|
-
const missingApiKey = !process.env.DATADOG_API_KEY && !process.env.DD_API_KEY;
|
|
683
|
-
const missingAppKey = !process.env.DATADOG_APP_KEY &&
|
|
684
|
-
!process.env.DATADOG_APPLICATION_KEY &&
|
|
685
|
-
!process.env.DD_APP_KEY &&
|
|
686
|
-
!process.env.DD_APPLICATION_KEY;
|
|
687
|
-
if (missingApiKey) {
|
|
688
|
-
log.error("No Datadog API key found in environment");
|
|
689
|
-
return {
|
|
690
|
-
content: [
|
|
691
|
-
{
|
|
692
|
-
type: "text",
|
|
693
|
-
text: "Error: No Datadog API key found. Please set DATADOG_API_KEY or DD_API_KEY environment variable.",
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
if (missingAppKey) {
|
|
699
|
-
log.error("No Datadog Application key found in environment");
|
|
700
|
-
return {
|
|
701
|
-
content: [
|
|
702
|
-
{
|
|
703
|
-
type: "text",
|
|
704
|
-
text: "Error: No Datadog Application key found. Please set DATADOG_APP_KEY or DD_APP_KEY environment variable.",
|
|
705
|
-
},
|
|
706
|
-
],
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
const compute = aggregation
|
|
711
|
-
? [{ aggregation, metric }]
|
|
712
|
-
: [{ aggregation: "count" }];
|
|
713
|
-
const result = await aggregateDatadogLogs(credentials, {
|
|
714
|
-
query,
|
|
715
|
-
source,
|
|
716
|
-
env,
|
|
717
|
-
service,
|
|
718
|
-
from,
|
|
719
|
-
to,
|
|
720
|
-
groupBy,
|
|
721
|
-
compute,
|
|
722
|
-
}, log);
|
|
723
|
-
if (!result.success) {
|
|
724
|
-
return {
|
|
725
|
-
content: [
|
|
726
|
-
{
|
|
727
|
-
type: "text",
|
|
728
|
-
text: `Error from Datadog Analytics API: ${result.error}`,
|
|
729
|
-
},
|
|
730
|
-
],
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
if (result.buckets.length === 0) {
|
|
734
|
-
return {
|
|
735
|
-
content: [
|
|
736
|
-
{
|
|
737
|
-
type: "text",
|
|
738
|
-
text: `No data found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}\nGrouped by: ${result.groupBy.join(", ")}`,
|
|
739
|
-
},
|
|
740
|
-
],
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
// Format buckets as a readable table
|
|
744
|
-
const formattedBuckets = result.buckets.map((bucket) => {
|
|
745
|
-
const byParts = Object.entries(bucket.by)
|
|
746
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
747
|
-
.join(", ");
|
|
748
|
-
const computeParts = Object.entries(bucket.computes)
|
|
749
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
750
|
-
.join(", ");
|
|
751
|
-
return ` ${byParts} => ${computeParts}`;
|
|
752
|
-
});
|
|
753
|
-
const resultText = [
|
|
754
|
-
`Query: ${result.query}`,
|
|
755
|
-
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
756
|
-
`Grouped by: ${result.groupBy.join(", ")}`,
|
|
757
|
-
`Found ${result.buckets.length} groups:`,
|
|
758
|
-
"",
|
|
759
|
-
...formattedBuckets,
|
|
760
|
-
].join("\n");
|
|
761
|
-
return {
|
|
762
|
-
content: [
|
|
763
|
-
{
|
|
764
|
-
type: "text",
|
|
765
|
-
text: resultText,
|
|
766
|
-
},
|
|
767
|
-
],
|
|
768
|
-
};
|
|
769
|
-
});
|
|
770
|
-
log.info("Registered tool: datadog_log_analytics");
|
|
771
|
-
// Datadog Monitors Tool
|
|
772
|
-
server.tool("datadog_monitors", "List and check Datadog monitors. Shows monitor status (Alert, Warn, No Data, OK), name, type, and tags. Useful for quickly checking if any monitors are alerting.", {
|
|
773
|
-
status: z
|
|
774
|
-
.array(z.enum(["Alert", "Warn", "No Data", "OK"]))
|
|
775
|
-
.optional()
|
|
776
|
-
.describe("Filter monitors by status. E.g., ['Alert', 'Warn'] to see only alerting monitors."),
|
|
777
|
-
tags: z
|
|
778
|
-
.array(z.string())
|
|
779
|
-
.optional()
|
|
780
|
-
.describe("Filter monitors by resource tags (tags on the monitored resources)."),
|
|
781
|
-
monitorTags: z
|
|
782
|
-
.array(z.string())
|
|
783
|
-
.optional()
|
|
784
|
-
.describe("Filter monitors by monitor tags (tags on the monitor itself)."),
|
|
785
|
-
name: z
|
|
786
|
-
.string()
|
|
787
|
-
.optional()
|
|
788
|
-
.describe("Filter monitors by name (partial match supported)."),
|
|
789
|
-
}, async ({ status, tags, monitorTags, name }) => {
|
|
790
|
-
log.info("Tool called: datadog_monitors");
|
|
791
|
-
const credentials = getDatadogCredentials();
|
|
792
|
-
if (!credentials) {
|
|
793
|
-
return {
|
|
794
|
-
content: [
|
|
795
|
-
{
|
|
796
|
-
type: "text",
|
|
797
|
-
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
798
|
-
},
|
|
799
|
-
],
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
const result = await listDatadogMonitors(credentials, { status, tags, monitorTags, name }, log);
|
|
803
|
-
if (!result.success) {
|
|
804
|
-
return {
|
|
805
|
-
content: [
|
|
806
|
-
{
|
|
807
|
-
type: "text",
|
|
808
|
-
text: `Error from Datadog Monitors API: ${result.error}`,
|
|
809
|
-
},
|
|
810
|
-
],
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
if (result.monitors.length === 0) {
|
|
814
|
-
return {
|
|
815
|
-
content: [
|
|
816
|
-
{
|
|
817
|
-
type: "text",
|
|
818
|
-
text: "No monitors found matching the specified criteria.",
|
|
819
|
-
},
|
|
820
|
-
],
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
// Group monitors by status for better readability
|
|
824
|
-
const byStatus = {};
|
|
825
|
-
for (const monitor of result.monitors) {
|
|
826
|
-
const status = monitor.status;
|
|
827
|
-
if (!byStatus[status]) {
|
|
828
|
-
byStatus[status] = [];
|
|
829
|
-
}
|
|
830
|
-
byStatus[status].push(monitor);
|
|
831
|
-
}
|
|
832
|
-
const statusOrder = ["Alert", "Warn", "No Data", "OK", "Unknown"];
|
|
833
|
-
const formattedMonitors = [];
|
|
834
|
-
for (const status of statusOrder) {
|
|
835
|
-
const monitors = byStatus[status];
|
|
836
|
-
if (monitors && monitors.length > 0) {
|
|
837
|
-
formattedMonitors.push(`\n## ${status} (${monitors.length})`);
|
|
838
|
-
for (const m of monitors) {
|
|
839
|
-
formattedMonitors.push(` - [${m.id}] ${m.name} (${m.type})`);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
const resultText = [
|
|
844
|
-
`Found ${result.monitors.length} monitors:`,
|
|
845
|
-
...formattedMonitors,
|
|
846
|
-
].join("\n");
|
|
847
|
-
return {
|
|
848
|
-
content: [
|
|
849
|
-
{
|
|
850
|
-
type: "text",
|
|
851
|
-
text: resultText,
|
|
852
|
-
},
|
|
853
|
-
],
|
|
854
|
-
};
|
|
855
|
-
});
|
|
856
|
-
log.info("Registered tool: datadog_monitors");
|
|
857
|
-
// Datadog Synthetics Tool
|
|
858
|
-
server.tool("datadog_synthetics", "List Datadog Synthetic tests and optionally get recent results for a specific test. Shows test status, type (api/browser), and locations.", {
|
|
859
|
-
type: z
|
|
860
|
-
.enum(["api", "browser"])
|
|
861
|
-
.optional()
|
|
862
|
-
.describe("Filter tests by type: 'api' or 'browser'."),
|
|
863
|
-
tags: z.array(z.string()).optional().describe("Filter tests by tags."),
|
|
864
|
-
testId: z
|
|
865
|
-
.string()
|
|
866
|
-
.optional()
|
|
867
|
-
.describe("If provided, fetches recent results for this specific test (public_id). Otherwise lists all tests."),
|
|
868
|
-
}, async ({ type, tags, testId }) => {
|
|
869
|
-
log.info("Tool called: datadog_synthetics");
|
|
870
|
-
const credentials = getDatadogCredentials();
|
|
871
|
-
if (!credentials) {
|
|
872
|
-
return {
|
|
873
|
-
content: [
|
|
874
|
-
{
|
|
875
|
-
type: "text",
|
|
876
|
-
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
877
|
-
},
|
|
878
|
-
],
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
// If testId is provided, get results for that specific test
|
|
882
|
-
if (testId) {
|
|
883
|
-
const result = await getDatadogSyntheticResults(credentials, testId, log);
|
|
884
|
-
if (!result.success) {
|
|
885
|
-
return {
|
|
886
|
-
content: [
|
|
887
|
-
{
|
|
888
|
-
type: "text",
|
|
889
|
-
text: `Error from Datadog Synthetics API: ${result.error}`,
|
|
890
|
-
},
|
|
891
|
-
],
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
if (result.results.length === 0) {
|
|
895
|
-
return {
|
|
896
|
-
content: [
|
|
897
|
-
{
|
|
898
|
-
type: "text",
|
|
899
|
-
text: `No recent results found for test: ${testId}`,
|
|
900
|
-
},
|
|
901
|
-
],
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
const passedCount = result.results.filter((r) => r.passed).length;
|
|
905
|
-
const failedCount = result.results.length - passedCount;
|
|
906
|
-
const formattedResults = result.results.slice(0, 10).map((r) => {
|
|
907
|
-
const date = new Date(r.checkTime * 1000).toISOString();
|
|
908
|
-
const status = r.passed ? "✓ PASSED" : "✗ FAILED";
|
|
909
|
-
return ` ${date} - ${status}`;
|
|
910
|
-
});
|
|
911
|
-
const resultText = [
|
|
912
|
-
`Results for test: ${testId}`,
|
|
913
|
-
`Recent: ${passedCount} passed, ${failedCount} failed (showing last ${Math.min(10, result.results.length)})`,
|
|
914
|
-
"",
|
|
915
|
-
...formattedResults,
|
|
916
|
-
].join("\n");
|
|
917
|
-
return {
|
|
918
|
-
content: [
|
|
919
|
-
{
|
|
920
|
-
type: "text",
|
|
921
|
-
text: resultText,
|
|
922
|
-
},
|
|
923
|
-
],
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
// Otherwise list all tests
|
|
927
|
-
const result = await listDatadogSynthetics(credentials, { type, tags }, log);
|
|
928
|
-
if (!result.success) {
|
|
929
|
-
return {
|
|
930
|
-
content: [
|
|
931
|
-
{
|
|
932
|
-
type: "text",
|
|
933
|
-
text: `Error from Datadog Synthetics API: ${result.error}`,
|
|
934
|
-
},
|
|
935
|
-
],
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
if (result.tests.length === 0) {
|
|
939
|
-
return {
|
|
940
|
-
content: [
|
|
941
|
-
{
|
|
942
|
-
type: "text",
|
|
943
|
-
text: "No synthetic tests found matching the specified criteria.",
|
|
944
|
-
},
|
|
945
|
-
],
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
// Group by status
|
|
949
|
-
const byStatus = {};
|
|
950
|
-
for (const test of result.tests) {
|
|
951
|
-
const status = test.status;
|
|
952
|
-
if (!byStatus[status]) {
|
|
953
|
-
byStatus[status] = [];
|
|
954
|
-
}
|
|
955
|
-
byStatus[status].push(test);
|
|
956
|
-
}
|
|
957
|
-
const formattedTests = [];
|
|
958
|
-
for (const [status, tests] of Object.entries(byStatus)) {
|
|
959
|
-
formattedTests.push(`\n## ${status} (${tests.length})`);
|
|
960
|
-
for (const t of tests) {
|
|
961
|
-
formattedTests.push(` - [${t.publicId}] ${t.name} (${t.type})`);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
const resultText = [
|
|
965
|
-
`Found ${result.tests.length} synthetic tests:`,
|
|
966
|
-
...formattedTests,
|
|
967
|
-
].join("\n");
|
|
968
|
-
return {
|
|
969
|
-
content: [
|
|
970
|
-
{
|
|
971
|
-
type: "text",
|
|
972
|
-
text: resultText,
|
|
973
|
-
},
|
|
974
|
-
],
|
|
975
|
-
};
|
|
976
|
-
});
|
|
977
|
-
log.info("Registered tool: datadog_synthetics");
|
|
978
|
-
// Datadog Metrics Tool
|
|
979
|
-
server.tool("datadog_metrics", "Query Datadog metrics. Returns timeseries data for the specified metric query. Useful for checking specific metric values.", {
|
|
980
|
-
query: z
|
|
981
|
-
.string()
|
|
982
|
-
.describe("Metric query. Format: 'aggregation:metric.name{tags}'. Examples: 'avg:system.cpu.user{*}', 'sum:aws.lambda.invocations{function:my-func}.as_count()', 'max:aws.lambda.duration{env:production}'."),
|
|
983
|
-
from: z
|
|
984
|
-
.string()
|
|
985
|
-
.optional()
|
|
986
|
-
.describe("Start time. Formats: relative ('1h', '30m', '1d'), or Unix timestamp. Defaults to '1h'."),
|
|
987
|
-
to: z
|
|
988
|
-
.string()
|
|
989
|
-
.optional()
|
|
990
|
-
.describe("End time. Formats: 'now' or Unix timestamp. Defaults to 'now'."),
|
|
991
|
-
}, async ({ query, from, to }) => {
|
|
992
|
-
log.info("Tool called: datadog_metrics");
|
|
993
|
-
const credentials = getDatadogCredentials();
|
|
994
|
-
if (!credentials) {
|
|
995
|
-
return {
|
|
996
|
-
content: [
|
|
997
|
-
{
|
|
998
|
-
type: "text",
|
|
999
|
-
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1000
|
-
},
|
|
1001
|
-
],
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
// Parse time parameters
|
|
1005
|
-
const now = Math.floor(Date.now() / 1000);
|
|
1006
|
-
let fromTs;
|
|
1007
|
-
let toTs;
|
|
1008
|
-
// Parse 'from' parameter
|
|
1009
|
-
const fromStr = from || "1h";
|
|
1010
|
-
if (fromStr.match(/^\d+$/)) {
|
|
1011
|
-
fromTs = parseInt(fromStr, 10);
|
|
1012
|
-
}
|
|
1013
|
-
else if (fromStr.match(/^(\d+)h$/)) {
|
|
1014
|
-
const hours = parseInt(fromStr.match(/^(\d+)h$/)[1], 10);
|
|
1015
|
-
fromTs = now - hours * 3600;
|
|
1016
|
-
}
|
|
1017
|
-
else if (fromStr.match(/^(\d+)m$/)) {
|
|
1018
|
-
const minutes = parseInt(fromStr.match(/^(\d+)m$/)[1], 10);
|
|
1019
|
-
fromTs = now - minutes * 60;
|
|
1020
|
-
}
|
|
1021
|
-
else if (fromStr.match(/^(\d+)d$/)) {
|
|
1022
|
-
const days = parseInt(fromStr.match(/^(\d+)d$/)[1], 10);
|
|
1023
|
-
fromTs = now - days * 86400;
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
fromTs = now - 3600; // Default 1 hour
|
|
1027
|
-
}
|
|
1028
|
-
// Parse 'to' parameter
|
|
1029
|
-
const toStr = to || "now";
|
|
1030
|
-
if (toStr === "now") {
|
|
1031
|
-
toTs = now;
|
|
1032
|
-
}
|
|
1033
|
-
else if (toStr.match(/^\d+$/)) {
|
|
1034
|
-
toTs = parseInt(toStr, 10);
|
|
1035
|
-
}
|
|
1036
|
-
else {
|
|
1037
|
-
toTs = now;
|
|
1038
|
-
}
|
|
1039
|
-
const result = await queryDatadogMetrics(credentials, { query, from: fromTs, to: toTs }, log);
|
|
1040
|
-
if (!result.success) {
|
|
1041
|
-
return {
|
|
1042
|
-
content: [
|
|
1043
|
-
{
|
|
1044
|
-
type: "text",
|
|
1045
|
-
text: `Error from Datadog Metrics API: ${result.error}`,
|
|
1046
|
-
},
|
|
1047
|
-
],
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
if (result.series.length === 0) {
|
|
1051
|
-
return {
|
|
1052
|
-
content: [
|
|
1053
|
-
{
|
|
1054
|
-
type: "text",
|
|
1055
|
-
text: `No data found for query: ${query}\nTime range: ${new Date(fromTs * 1000).toISOString()} to ${new Date(toTs * 1000).toISOString()}`,
|
|
1056
|
-
},
|
|
1057
|
-
],
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
const formattedSeries = result.series.map((s) => {
|
|
1061
|
-
const points = s.pointlist.slice(-5); // Last 5 points
|
|
1062
|
-
const formattedPoints = points
|
|
1063
|
-
.map(([ts, val]) => {
|
|
1064
|
-
const date = new Date(ts).toISOString();
|
|
1065
|
-
return ` ${date}: ${val !== null ? val.toFixed(4) : "null"}`;
|
|
1066
|
-
})
|
|
1067
|
-
.join("\n");
|
|
1068
|
-
return `\n ${s.metric} (${s.scope})${s.unit ? ` [${s.unit}]` : ""}:\n${formattedPoints}`;
|
|
1069
|
-
});
|
|
1070
|
-
const resultText = [
|
|
1071
|
-
`Query: ${query}`,
|
|
1072
|
-
`Time range: ${new Date(fromTs * 1000).toISOString()} to ${new Date(toTs * 1000).toISOString()}`,
|
|
1073
|
-
`Found ${result.series.length} series (showing last 5 points each):`,
|
|
1074
|
-
...formattedSeries,
|
|
1075
|
-
].join("\n");
|
|
1076
|
-
return {
|
|
1077
|
-
content: [
|
|
1078
|
-
{
|
|
1079
|
-
type: "text",
|
|
1080
|
-
text: resultText,
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1083
|
-
};
|
|
1084
|
-
});
|
|
1085
|
-
log.info("Registered tool: datadog_metrics");
|
|
1086
|
-
// Datadog RUM Tool
|
|
1087
|
-
server.tool("datadog_rum", "Search Datadog RUM (Real User Monitoring) events. Find user sessions, page views, errors, and actions. Useful for debugging frontend issues and understanding user behavior.", {
|
|
1088
|
-
query: z
|
|
1089
|
-
.string()
|
|
1090
|
-
.optional()
|
|
1091
|
-
.describe("RUM search query. E.g., '@type:error', '@session.id:abc123', '@view.url:*checkout*'. Defaults to '*' (all events)."),
|
|
1092
|
-
from: z
|
|
1093
|
-
.string()
|
|
1094
|
-
.optional()
|
|
1095
|
-
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
1096
|
-
to: z
|
|
1097
|
-
.string()
|
|
1098
|
-
.optional()
|
|
1099
|
-
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
1100
|
-
limit: z
|
|
1101
|
-
.number()
|
|
1102
|
-
.optional()
|
|
1103
|
-
.describe("Max events to return (1-1000). Defaults to 50."),
|
|
1104
|
-
}, async ({ query, from, to, limit }) => {
|
|
1105
|
-
log.info("Tool called: datadog_rum");
|
|
1106
|
-
const credentials = getDatadogCredentials();
|
|
1107
|
-
if (!credentials) {
|
|
1108
|
-
return {
|
|
1109
|
-
content: [
|
|
1110
|
-
{
|
|
1111
|
-
type: "text",
|
|
1112
|
-
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1113
|
-
},
|
|
1114
|
-
],
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
const result = await searchDatadogRum(credentials, { query, from, to, limit }, log);
|
|
1118
|
-
if (!result.success) {
|
|
1119
|
-
return {
|
|
1120
|
-
content: [
|
|
1121
|
-
{
|
|
1122
|
-
type: "text",
|
|
1123
|
-
text: `Error from Datadog RUM API: ${result.error}`,
|
|
1124
|
-
},
|
|
1125
|
-
],
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
if (result.events.length === 0) {
|
|
1129
|
-
return {
|
|
1130
|
-
content: [
|
|
1131
|
-
{
|
|
1132
|
-
type: "text",
|
|
1133
|
-
text: `No RUM events found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
// Group events by type for better readability
|
|
1139
|
-
const byType = {};
|
|
1140
|
-
for (const event of result.events) {
|
|
1141
|
-
const type = event.type;
|
|
1142
|
-
if (!byType[type]) {
|
|
1143
|
-
byType[type] = [];
|
|
1144
|
-
}
|
|
1145
|
-
byType[type].push(event);
|
|
1146
|
-
}
|
|
1147
|
-
const formattedEvents = [];
|
|
1148
|
-
for (const [type, events] of Object.entries(byType)) {
|
|
1149
|
-
formattedEvents.push(`\n## ${type} (${events.length})`);
|
|
1150
|
-
for (const e of events.slice(0, 10)) {
|
|
1151
|
-
// Limit per type
|
|
1152
|
-
const parts = [e.timestamp];
|
|
1153
|
-
if (e.viewName || e.viewUrl) {
|
|
1154
|
-
parts.push(e.viewName || e.viewUrl || "");
|
|
1155
|
-
}
|
|
1156
|
-
if (e.errorMessage) {
|
|
1157
|
-
parts.push(`Error: ${e.errorMessage}`);
|
|
1158
|
-
}
|
|
1159
|
-
if (e.sessionId) {
|
|
1160
|
-
parts.push(`Session: ${e.sessionId.substring(0, 8)}...`);
|
|
1161
|
-
}
|
|
1162
|
-
formattedEvents.push(` - ${parts.join(" | ")}`);
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
const resultText = [
|
|
1166
|
-
`Query: ${result.query}`,
|
|
1167
|
-
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1168
|
-
`Found ${result.events.length} RUM events:`,
|
|
1169
|
-
...formattedEvents,
|
|
1170
|
-
].join("\n");
|
|
1171
|
-
return {
|
|
1172
|
-
content: [
|
|
1173
|
-
{
|
|
1174
|
-
type: "text",
|
|
1175
|
-
text: resultText,
|
|
1176
|
-
},
|
|
1177
|
-
],
|
|
1178
|
-
};
|
|
1179
|
-
});
|
|
1180
|
-
log.info("Registered tool: datadog_rum");
|
|
1181
|
-
// LLM Debug Tools
|
|
1182
|
-
server.tool("llm_debug_call", "Make a debug LLM API call and inspect the raw response. Useful for understanding how each provider formats responses, especially for reasoning/thinking content. Returns full history, raw responses, and extracted reasoning.", {
|
|
1183
|
-
provider: z
|
|
1184
|
-
.enum(["anthropic", "gemini", "openai", "openrouter"])
|
|
1185
|
-
.describe("LLM provider to call"),
|
|
1186
|
-
model: z
|
|
1187
|
-
.string()
|
|
1188
|
-
.optional()
|
|
1189
|
-
.describe("Model to use. If not provided, uses a sensible default. For reasoning tests, try 'o3-mini' with openai."),
|
|
1190
|
-
message: z
|
|
1191
|
-
.string()
|
|
1192
|
-
.describe("Message to send to the LLM. For reasoning tests, try something that requires thinking like 'What is 15 * 17? Think step by step.'"),
|
|
1193
|
-
}, async ({ provider, model, message }) => {
|
|
1194
|
-
log.info(`Tool called: llm_debug_call (provider: ${provider})`);
|
|
1195
|
-
const result = await debugLlmCall({ provider: provider, model, message }, log);
|
|
1196
|
-
if (!result.success) {
|
|
1197
|
-
return {
|
|
1198
|
-
content: [
|
|
1199
|
-
{
|
|
1200
|
-
type: "text",
|
|
1201
|
-
text: `Error calling ${provider}: ${result.error}`,
|
|
1202
|
-
},
|
|
1203
|
-
],
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
const sections = [
|
|
1207
|
-
`## LLM Debug Call Result`,
|
|
1208
|
-
`Provider: ${result.provider}`,
|
|
1209
|
-
`Model: ${result.model}`,
|
|
1210
|
-
``,
|
|
1211
|
-
`### Content`,
|
|
1212
|
-
result.content || "(no content)",
|
|
1213
|
-
``,
|
|
1214
|
-
`### Reasoning (${result.reasoning?.length || 0} items, ${result.reasoningTokens || 0} tokens)`,
|
|
1215
|
-
result.reasoning && result.reasoning.length > 0
|
|
1216
|
-
? result.reasoning.map((r, i) => `[${i}] ${r}`).join("\n")
|
|
1217
|
-
: "(no reasoning extracted)",
|
|
1218
|
-
``,
|
|
1219
|
-
`### Usage`,
|
|
1220
|
-
JSON.stringify(result.usage, null, 2),
|
|
1221
|
-
``,
|
|
1222
|
-
`### History (${result.history?.length || 0} items)`,
|
|
1223
|
-
JSON.stringify(result.history, null, 2),
|
|
1224
|
-
``,
|
|
1225
|
-
`### Raw Responses (${result.rawResponses?.length || 0} items)`,
|
|
1226
|
-
JSON.stringify(result.rawResponses, null, 2),
|
|
1227
|
-
];
|
|
1228
|
-
return {
|
|
1229
|
-
content: [
|
|
1230
|
-
{
|
|
1231
|
-
type: "text",
|
|
1232
|
-
text: sections.join("\n"),
|
|
1233
|
-
},
|
|
1234
|
-
],
|
|
1235
|
-
};
|
|
1236
|
-
});
|
|
1237
|
-
log.info("Registered tool: llm_debug_call");
|
|
1238
|
-
server.tool("llm_list_providers", "List available LLM providers with their default and reasoning-capable models.", {}, async () => {
|
|
1239
|
-
log.info("Tool called: llm_list_providers");
|
|
1240
|
-
const { providers } = listLlmProviders();
|
|
1241
|
-
const formatted = providers.map((p) => {
|
|
1242
|
-
const reasoningNote = p.reasoningModels.length > 0
|
|
1243
|
-
? `Reasoning models: ${p.reasoningModels.join(", ")}`
|
|
1244
|
-
: "No known reasoning models";
|
|
1245
|
-
return `- ${p.name}: default=${p.defaultModel}, ${reasoningNote}`;
|
|
1246
|
-
});
|
|
1247
|
-
return {
|
|
1248
|
-
content: [
|
|
1249
|
-
{
|
|
1250
|
-
type: "text",
|
|
1251
|
-
text: [
|
|
1252
|
-
"## Available LLM Providers",
|
|
1253
|
-
"",
|
|
1254
|
-
...formatted,
|
|
1255
|
-
"",
|
|
1256
|
-
"Use llm_debug_call to test responses from any provider.",
|
|
1257
|
-
].join("\n"),
|
|
1258
|
-
},
|
|
1259
|
-
],
|
|
1260
|
-
};
|
|
1261
|
-
});
|
|
1262
|
-
log.info("Registered tool: llm_list_providers");
|
|
1263
|
-
// AWS CLI Tools
|
|
1264
|
-
// AWS List Profiles
|
|
1265
|
-
server.tool("aws_list_profiles", "List available AWS profiles from ~/.aws/config and credentials.", {}, async () => {
|
|
1266
|
-
log.info("Tool called: aws_list_profiles");
|
|
1267
|
-
const result = await listAwsProfiles(log);
|
|
1268
|
-
if (!result.success) {
|
|
1269
|
-
return {
|
|
1270
|
-
content: [
|
|
1271
|
-
{
|
|
1272
|
-
type: "text",
|
|
1273
|
-
text: `Error listing profiles: ${result.error}`,
|
|
1274
|
-
},
|
|
1275
|
-
],
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
const profiles = result.data || [];
|
|
1279
|
-
if (profiles.length === 0) {
|
|
1280
|
-
return {
|
|
1281
|
-
content: [
|
|
1282
|
-
{
|
|
1283
|
-
type: "text",
|
|
1284
|
-
text: "No AWS profiles found. Configure profiles in ~/.aws/config or ~/.aws/credentials.",
|
|
1285
|
-
},
|
|
1286
|
-
],
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
const formatted = profiles
|
|
1290
|
-
.map((p) => `- ${p.name} (${p.source})`)
|
|
1291
|
-
.join("\n");
|
|
1292
|
-
return {
|
|
1293
|
-
content: [
|
|
1294
|
-
{
|
|
1295
|
-
type: "text",
|
|
1296
|
-
text: [
|
|
1297
|
-
`Found ${profiles.length} AWS profiles:`,
|
|
1298
|
-
"",
|
|
1299
|
-
formatted,
|
|
1300
|
-
"",
|
|
1301
|
-
"Use the 'profile' parameter in other AWS tools to specify which profile to use.",
|
|
1302
|
-
].join("\n"),
|
|
1303
|
-
},
|
|
1304
|
-
],
|
|
1305
|
-
};
|
|
1306
|
-
});
|
|
1307
|
-
log.info("Registered tool: aws_list_profiles");
|
|
1308
|
-
// Step Functions: List Executions
|
|
1309
|
-
server.tool("aws_stepfunctions_list_executions", "List Step Function executions for a state machine. Useful for finding stuck or running executions.", {
|
|
1310
|
-
stateMachineArn: z.string().describe("ARN of the state machine"),
|
|
1311
|
-
statusFilter: z
|
|
1312
|
-
.enum([
|
|
1313
|
-
"RUNNING",
|
|
1314
|
-
"SUCCEEDED",
|
|
1315
|
-
"FAILED",
|
|
1316
|
-
"TIMED_OUT",
|
|
1317
|
-
"ABORTED",
|
|
1318
|
-
"PENDING_REDRIVE",
|
|
1319
|
-
])
|
|
1320
|
-
.optional()
|
|
1321
|
-
.describe("Filter by execution status"),
|
|
1322
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1323
|
-
region: z.string().optional().describe("AWS region"),
|
|
1324
|
-
maxResults: z
|
|
1325
|
-
.number()
|
|
1326
|
-
.optional()
|
|
1327
|
-
.describe("Max results (1-1000, default 100)"),
|
|
1328
|
-
}, async ({ stateMachineArn, statusFilter, profile, region, maxResults }) => {
|
|
1329
|
-
log.info("Tool called: aws_stepfunctions_list_executions");
|
|
1330
|
-
const result = await listStepFunctionExecutions({
|
|
1331
|
-
stateMachineArn,
|
|
1332
|
-
statusFilter,
|
|
1333
|
-
profile,
|
|
1334
|
-
region,
|
|
1335
|
-
maxResults,
|
|
1336
|
-
}, log);
|
|
1337
|
-
if (!result.success) {
|
|
1338
|
-
return {
|
|
1339
|
-
content: [
|
|
1340
|
-
{
|
|
1341
|
-
type: "text",
|
|
1342
|
-
text: `Error: ${result.error}`,
|
|
1343
|
-
},
|
|
1344
|
-
],
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
const executions = result.data?.executions || [];
|
|
1348
|
-
if (executions.length === 0) {
|
|
1349
|
-
return {
|
|
1350
|
-
content: [
|
|
1351
|
-
{
|
|
1352
|
-
type: "text",
|
|
1353
|
-
text: `No ${statusFilter || ""} executions found for state machine.`,
|
|
1354
|
-
},
|
|
1355
|
-
],
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
const formatted = executions
|
|
1359
|
-
.map((e) => `- ${e.name} (${e.status}) started ${e.startDate}`)
|
|
1360
|
-
.join("\n");
|
|
1361
|
-
return {
|
|
1362
|
-
content: [
|
|
1363
|
-
{
|
|
1364
|
-
type: "text",
|
|
1365
|
-
text: [
|
|
1366
|
-
`Found ${executions.length} executions:`,
|
|
1367
|
-
"",
|
|
1368
|
-
formatted,
|
|
1369
|
-
"",
|
|
1370
|
-
"Use aws_stepfunctions_stop_execution to stop running executions.",
|
|
1371
|
-
].join("\n"),
|
|
1372
|
-
},
|
|
1373
|
-
],
|
|
1374
|
-
};
|
|
1375
|
-
});
|
|
1376
|
-
log.info("Registered tool: aws_stepfunctions_list_executions");
|
|
1377
|
-
// Step Functions: Stop Execution
|
|
1378
|
-
server.tool("aws_stepfunctions_stop_execution", "Stop a running Step Function execution. Use with caution - this will abort the workflow.", {
|
|
1379
|
-
executionArn: z.string().describe("ARN of the execution to stop"),
|
|
1380
|
-
cause: z
|
|
1381
|
-
.string()
|
|
1382
|
-
.optional()
|
|
1383
|
-
.describe("Description of why the execution was stopped"),
|
|
1384
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1385
|
-
region: z.string().optional().describe("AWS region"),
|
|
1386
|
-
}, async ({ executionArn, cause, profile, region }) => {
|
|
1387
|
-
log.info("Tool called: aws_stepfunctions_stop_execution");
|
|
1388
|
-
const result = await stopStepFunctionExecution({
|
|
1389
|
-
executionArn,
|
|
1390
|
-
cause,
|
|
1391
|
-
profile,
|
|
1392
|
-
region,
|
|
1393
|
-
}, log);
|
|
1394
|
-
if (!result.success) {
|
|
1395
|
-
return {
|
|
1396
|
-
content: [
|
|
1397
|
-
{
|
|
1398
|
-
type: "text",
|
|
1399
|
-
text: `Error stopping execution: ${result.error}`,
|
|
1400
|
-
},
|
|
1401
|
-
],
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
return {
|
|
1405
|
-
content: [
|
|
1406
|
-
{
|
|
1407
|
-
type: "text",
|
|
1408
|
-
text: `Execution stopped successfully at ${result.data?.stopDate || "unknown time"}.`,
|
|
1409
|
-
},
|
|
1410
|
-
],
|
|
1411
|
-
};
|
|
1412
|
-
});
|
|
1413
|
-
log.info("Registered tool: aws_stepfunctions_stop_execution");
|
|
1414
|
-
// Lambda: List Functions
|
|
1415
|
-
server.tool("aws_lambda_list_functions", "List Lambda functions in the account. Filter by function name prefix.", {
|
|
1416
|
-
functionNamePrefix: z
|
|
1417
|
-
.string()
|
|
1418
|
-
.optional()
|
|
1419
|
-
.describe("Filter by function name prefix"),
|
|
1420
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1421
|
-
region: z.string().optional().describe("AWS region"),
|
|
1422
|
-
maxResults: z.number().optional().describe("Max results to return"),
|
|
1423
|
-
}, async ({ functionNamePrefix, profile, region, maxResults }) => {
|
|
1424
|
-
log.info("Tool called: aws_lambda_list_functions");
|
|
1425
|
-
const result = await listLambdaFunctions({
|
|
1426
|
-
functionNamePrefix,
|
|
1427
|
-
profile,
|
|
1428
|
-
region,
|
|
1429
|
-
maxResults,
|
|
1430
|
-
}, log);
|
|
1431
|
-
if (!result.success) {
|
|
1432
|
-
return {
|
|
1433
|
-
content: [
|
|
1434
|
-
{
|
|
1435
|
-
type: "text",
|
|
1436
|
-
text: `Error: ${result.error}`,
|
|
1437
|
-
},
|
|
1438
|
-
],
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
const functions = result.data?.Functions || [];
|
|
1442
|
-
if (functions.length === 0) {
|
|
1443
|
-
return {
|
|
1444
|
-
content: [
|
|
1445
|
-
{
|
|
1446
|
-
type: "text",
|
|
1447
|
-
text: functionNamePrefix
|
|
1448
|
-
? `No functions found with prefix "${functionNamePrefix}".`
|
|
1449
|
-
: "No Lambda functions found in this account/region.",
|
|
1450
|
-
},
|
|
1451
|
-
],
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
const formatted = functions
|
|
1455
|
-
.map((f) => `- ${f.FunctionName} (${f.Runtime || "unknown runtime"}, ${f.MemorySize}MB)`)
|
|
1456
|
-
.join("\n");
|
|
1457
|
-
return {
|
|
1458
|
-
content: [
|
|
1459
|
-
{
|
|
1460
|
-
type: "text",
|
|
1461
|
-
text: [
|
|
1462
|
-
`Found ${functions.length} Lambda functions:`,
|
|
1463
|
-
"",
|
|
1464
|
-
formatted,
|
|
1465
|
-
"",
|
|
1466
|
-
"Use aws_lambda_get_function for details on a specific function.",
|
|
1467
|
-
].join("\n"),
|
|
1468
|
-
},
|
|
1469
|
-
],
|
|
1470
|
-
};
|
|
1471
|
-
});
|
|
1472
|
-
log.info("Registered tool: aws_lambda_list_functions");
|
|
1473
|
-
// Lambda: Get Function
|
|
1474
|
-
server.tool("aws_lambda_get_function", "Get configuration and details for a specific Lambda function.", {
|
|
1475
|
-
functionName: z.string().describe("Function name or ARN"),
|
|
1476
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1477
|
-
region: z.string().optional().describe("AWS region"),
|
|
1478
|
-
}, async ({ functionName, profile, region }) => {
|
|
1479
|
-
log.info("Tool called: aws_lambda_get_function");
|
|
1480
|
-
const result = await getLambdaFunction({
|
|
1481
|
-
functionName,
|
|
1482
|
-
profile,
|
|
1483
|
-
region,
|
|
1484
|
-
}, log);
|
|
1485
|
-
if (!result.success) {
|
|
1486
|
-
return {
|
|
1487
|
-
content: [
|
|
1488
|
-
{
|
|
1489
|
-
type: "text",
|
|
1490
|
-
text: `Error: ${result.error}`,
|
|
1491
|
-
},
|
|
1492
|
-
],
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
return {
|
|
1496
|
-
content: [
|
|
1497
|
-
{
|
|
1498
|
-
type: "text",
|
|
1499
|
-
text: JSON.stringify(result.data, null, 2),
|
|
1500
|
-
},
|
|
1501
|
-
],
|
|
1502
|
-
};
|
|
1503
|
-
});
|
|
1504
|
-
log.info("Registered tool: aws_lambda_get_function");
|
|
1505
|
-
// CloudWatch Logs: Filter Log Events
|
|
1506
|
-
server.tool("aws_logs_filter_log_events", "Search CloudWatch Logs for a log group. Filter by pattern and time range.", {
|
|
1507
|
-
logGroupName: z
|
|
1508
|
-
.string()
|
|
1509
|
-
.describe("Log group name (e.g., /aws/lambda/my-function)"),
|
|
1510
|
-
filterPattern: z
|
|
1511
|
-
.string()
|
|
1512
|
-
.optional()
|
|
1513
|
-
.describe("CloudWatch filter pattern (e.g., 'ERROR', '{ $.level = \"error\" }')"),
|
|
1514
|
-
startTime: z
|
|
1515
|
-
.string()
|
|
1516
|
-
.optional()
|
|
1517
|
-
.describe("Start time (ISO 8601 or relative like 'now-1h'). Defaults to 'now-15m'."),
|
|
1518
|
-
endTime: z
|
|
1519
|
-
.string()
|
|
1520
|
-
.optional()
|
|
1521
|
-
.describe("End time (ISO 8601 or 'now'). Defaults to 'now'."),
|
|
1522
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1523
|
-
region: z.string().optional().describe("AWS region"),
|
|
1524
|
-
limit: z
|
|
1525
|
-
.number()
|
|
1526
|
-
.optional()
|
|
1527
|
-
.describe("Max events to return (default 100)"),
|
|
1528
|
-
}, async ({ logGroupName, filterPattern, startTime, endTime, profile, region, limit, }) => {
|
|
1529
|
-
log.info("Tool called: aws_logs_filter_log_events");
|
|
1530
|
-
const result = await filterLogEvents({
|
|
1531
|
-
logGroupName,
|
|
1532
|
-
filterPattern,
|
|
1533
|
-
startTime: startTime || "now-15m",
|
|
1534
|
-
endTime: endTime || "now",
|
|
1535
|
-
limit: limit || 100,
|
|
1536
|
-
profile,
|
|
1537
|
-
region,
|
|
1538
|
-
}, log);
|
|
1539
|
-
if (!result.success) {
|
|
1540
|
-
return {
|
|
1541
|
-
content: [
|
|
1542
|
-
{
|
|
1543
|
-
type: "text",
|
|
1544
|
-
text: `Error: ${result.error}`,
|
|
1545
|
-
},
|
|
1546
|
-
],
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
const events = result.data?.events || [];
|
|
1550
|
-
if (events.length === 0) {
|
|
1551
|
-
return {
|
|
1552
|
-
content: [
|
|
1553
|
-
{
|
|
1554
|
-
type: "text",
|
|
1555
|
-
text: `No log events found matching the filter in ${logGroupName}.`,
|
|
1556
|
-
},
|
|
1557
|
-
],
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
const formatted = events
|
|
1561
|
-
.map((e) => {
|
|
1562
|
-
const timestamp = new Date(e.timestamp).toISOString();
|
|
1563
|
-
return `[${timestamp}] ${e.message}`;
|
|
1564
|
-
})
|
|
1565
|
-
.join("\n");
|
|
1566
|
-
return {
|
|
1567
|
-
content: [
|
|
1568
|
-
{
|
|
1569
|
-
type: "text",
|
|
1570
|
-
text: [`Found ${events.length} log events:`, "", formatted].join("\n"),
|
|
1571
|
-
},
|
|
1572
|
-
],
|
|
1573
|
-
};
|
|
1574
|
-
});
|
|
1575
|
-
log.info("Registered tool: aws_logs_filter_log_events");
|
|
1576
|
-
// S3: List Objects
|
|
1577
|
-
server.tool("aws_s3_list_objects", "List objects in an S3 bucket with optional prefix filtering.", {
|
|
1578
|
-
bucket: z.string().describe("S3 bucket name"),
|
|
1579
|
-
prefix: z.string().optional().describe("Object key prefix filter"),
|
|
1580
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1581
|
-
region: z.string().optional().describe("AWS region"),
|
|
1582
|
-
maxResults: z.number().optional().describe("Max results to return"),
|
|
1583
|
-
}, async ({ bucket, prefix, profile, region, maxResults }) => {
|
|
1584
|
-
log.info("Tool called: aws_s3_list_objects");
|
|
1585
|
-
const result = await listS3Objects({
|
|
1586
|
-
bucket,
|
|
1587
|
-
prefix,
|
|
1588
|
-
profile,
|
|
1589
|
-
region,
|
|
1590
|
-
maxResults,
|
|
1591
|
-
}, log);
|
|
1592
|
-
if (!result.success) {
|
|
1593
|
-
return {
|
|
1594
|
-
content: [
|
|
1595
|
-
{
|
|
1596
|
-
type: "text",
|
|
1597
|
-
text: `Error: ${result.error}`,
|
|
1598
|
-
},
|
|
1599
|
-
],
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
const objects = result.data?.Contents || [];
|
|
1603
|
-
if (objects.length === 0) {
|
|
1604
|
-
return {
|
|
1605
|
-
content: [
|
|
1606
|
-
{
|
|
1607
|
-
type: "text",
|
|
1608
|
-
text: prefix
|
|
1609
|
-
? `No objects found with prefix "${prefix}" in bucket ${bucket}.`
|
|
1610
|
-
: `Bucket ${bucket} is empty.`,
|
|
1611
|
-
},
|
|
1612
|
-
],
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
const formatted = objects
|
|
1616
|
-
.map((o) => {
|
|
1617
|
-
const size = o.Size < 1024
|
|
1618
|
-
? `${o.Size}B`
|
|
1619
|
-
: o.Size < 1024 * 1024
|
|
1620
|
-
? `${(o.Size / 1024).toFixed(1)}KB`
|
|
1621
|
-
: `${(o.Size / (1024 * 1024)).toFixed(1)}MB`;
|
|
1622
|
-
return `- ${o.Key} (${size}, ${o.LastModified})`;
|
|
1623
|
-
})
|
|
1624
|
-
.join("\n");
|
|
1625
|
-
return {
|
|
1626
|
-
content: [
|
|
1627
|
-
{
|
|
1628
|
-
type: "text",
|
|
1629
|
-
text: [
|
|
1630
|
-
`Found ${objects.length} objects in ${bucket}:`,
|
|
1631
|
-
"",
|
|
1632
|
-
formatted,
|
|
1633
|
-
].join("\n"),
|
|
1634
|
-
},
|
|
1635
|
-
],
|
|
1636
|
-
};
|
|
1637
|
-
});
|
|
1638
|
-
log.info("Registered tool: aws_s3_list_objects");
|
|
1639
|
-
// CloudFormation: Describe Stack
|
|
1640
|
-
server.tool("aws_cloudformation_describe_stack", "Get details and status of a CloudFormation stack.", {
|
|
1641
|
-
stackName: z.string().describe("Stack name or ARN"),
|
|
1642
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1643
|
-
region: z.string().optional().describe("AWS region"),
|
|
1644
|
-
}, async ({ stackName, profile, region }) => {
|
|
1645
|
-
log.info("Tool called: aws_cloudformation_describe_stack");
|
|
1646
|
-
const result = await describeStack({
|
|
1647
|
-
stackName,
|
|
1648
|
-
profile,
|
|
1649
|
-
region,
|
|
1650
|
-
}, log);
|
|
1651
|
-
if (!result.success) {
|
|
1652
|
-
return {
|
|
1653
|
-
content: [
|
|
1654
|
-
{
|
|
1655
|
-
type: "text",
|
|
1656
|
-
text: `Error: ${result.error}`,
|
|
1657
|
-
},
|
|
1658
|
-
],
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
const stack = result.data?.Stacks?.[0];
|
|
1662
|
-
if (!stack) {
|
|
1663
|
-
return {
|
|
1664
|
-
content: [
|
|
1665
|
-
{
|
|
1666
|
-
type: "text",
|
|
1667
|
-
text: `Stack "${stackName}" not found.`,
|
|
1668
|
-
},
|
|
1669
|
-
],
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
const outputs = stack.Outputs?.map((o) => ` - ${o.OutputKey}: ${o.OutputValue}`).join("\n");
|
|
1673
|
-
const params = stack.Parameters?.map((p) => ` - ${p.ParameterKey}: ${p.ParameterValue}`).join("\n");
|
|
1674
|
-
return {
|
|
1675
|
-
content: [
|
|
1676
|
-
{
|
|
1677
|
-
type: "text",
|
|
1678
|
-
text: [
|
|
1679
|
-
`Stack: ${stack.StackName}`,
|
|
1680
|
-
`Status: ${stack.StackStatus}`,
|
|
1681
|
-
stack.StackStatusReason
|
|
1682
|
-
? `Reason: ${stack.StackStatusReason}`
|
|
1683
|
-
: null,
|
|
1684
|
-
`Created: ${stack.CreationTime}`,
|
|
1685
|
-
stack.LastUpdatedTime
|
|
1686
|
-
? `Last Updated: ${stack.LastUpdatedTime}`
|
|
1687
|
-
: null,
|
|
1688
|
-
stack.Description ? `Description: ${stack.Description}` : null,
|
|
1689
|
-
"",
|
|
1690
|
-
outputs ? `Outputs:\n${outputs}` : null,
|
|
1691
|
-
params ? `Parameters:\n${params}` : null,
|
|
1692
|
-
]
|
|
1693
|
-
.filter(Boolean)
|
|
1694
|
-
.join("\n"),
|
|
1695
|
-
},
|
|
1696
|
-
],
|
|
1697
|
-
};
|
|
1698
|
-
});
|
|
1699
|
-
log.info("Registered tool: aws_cloudformation_describe_stack");
|
|
1700
|
-
// DynamoDB: Describe Table
|
|
1701
|
-
server.tool("aws_dynamodb_describe_table", "Get metadata about a DynamoDB table including key schema, indexes, and provisioned capacity.", {
|
|
1702
|
-
tableName: z.string().describe("DynamoDB table name"),
|
|
1703
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1704
|
-
region: z.string().optional().describe("AWS region"),
|
|
1705
|
-
}, async ({ tableName, profile, region }) => {
|
|
1706
|
-
log.info("Tool called: aws_dynamodb_describe_table");
|
|
1707
|
-
const result = await describeDynamoDBTable({
|
|
1708
|
-
tableName,
|
|
1709
|
-
profile,
|
|
1710
|
-
region,
|
|
1711
|
-
}, log);
|
|
1712
|
-
if (!result.success) {
|
|
1713
|
-
return {
|
|
1714
|
-
content: [
|
|
1715
|
-
{
|
|
1716
|
-
type: "text",
|
|
1717
|
-
text: `Error: ${result.error}`,
|
|
1718
|
-
},
|
|
1719
|
-
],
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
return {
|
|
1723
|
-
content: [
|
|
1724
|
-
{
|
|
1725
|
-
type: "text",
|
|
1726
|
-
text: JSON.stringify(result.data?.Table, null, 2),
|
|
1727
|
-
},
|
|
1728
|
-
],
|
|
1729
|
-
};
|
|
1730
|
-
});
|
|
1731
|
-
log.info("Registered tool: aws_dynamodb_describe_table");
|
|
1732
|
-
// DynamoDB: Scan
|
|
1733
|
-
server.tool("aws_dynamodb_scan", "Scan a DynamoDB table. Use sparingly on large tables - prefer query when possible.", {
|
|
1734
|
-
tableName: z.string().describe("DynamoDB table name"),
|
|
1735
|
-
filterExpression: z
|
|
1736
|
-
.string()
|
|
1737
|
-
.optional()
|
|
1738
|
-
.describe("Filter expression (e.g., 'status = :s')"),
|
|
1739
|
-
expressionAttributeValues: z
|
|
1740
|
-
.string()
|
|
1741
|
-
.optional()
|
|
1742
|
-
.describe('JSON object of attribute values (e.g., \'{\\":s\\":{\\"S\\":\\"active\\"}}\')'),
|
|
1743
|
-
limit: z.number().optional().describe("Max items to return (default 25)"),
|
|
1744
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1745
|
-
region: z.string().optional().describe("AWS region"),
|
|
1746
|
-
}, async ({ tableName, filterExpression, expressionAttributeValues, limit, profile, region, }) => {
|
|
1747
|
-
log.info("Tool called: aws_dynamodb_scan");
|
|
1748
|
-
const result = await scanDynamoDB({
|
|
1749
|
-
tableName,
|
|
1750
|
-
filterExpression,
|
|
1751
|
-
expressionAttributeValues,
|
|
1752
|
-
limit: limit || 25,
|
|
1753
|
-
profile,
|
|
1754
|
-
region,
|
|
1755
|
-
}, log);
|
|
1756
|
-
if (!result.success) {
|
|
1757
|
-
return {
|
|
1758
|
-
content: [
|
|
1759
|
-
{
|
|
1760
|
-
type: "text",
|
|
1761
|
-
text: `Error: ${result.error}`,
|
|
1762
|
-
},
|
|
1763
|
-
],
|
|
1764
|
-
};
|
|
1765
|
-
}
|
|
1766
|
-
const items = result.data?.Items || [];
|
|
1767
|
-
if (items.length === 0) {
|
|
1768
|
-
return {
|
|
1769
|
-
content: [
|
|
1770
|
-
{
|
|
1771
|
-
type: "text",
|
|
1772
|
-
text: `No items found in table ${tableName}.`,
|
|
1773
|
-
},
|
|
1774
|
-
],
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
return {
|
|
1778
|
-
content: [
|
|
1779
|
-
{
|
|
1780
|
-
type: "text",
|
|
1781
|
-
text: [
|
|
1782
|
-
`Found ${items.length} items:`,
|
|
1783
|
-
"",
|
|
1784
|
-
JSON.stringify(items, null, 2),
|
|
1785
|
-
].join("\n"),
|
|
1786
|
-
},
|
|
1787
|
-
],
|
|
1788
|
-
};
|
|
1789
|
-
});
|
|
1790
|
-
log.info("Registered tool: aws_dynamodb_scan");
|
|
1791
|
-
// DynamoDB: Query
|
|
1792
|
-
server.tool("aws_dynamodb_query", "Query a DynamoDB table by partition key. More efficient than scan for targeted lookups.", {
|
|
1793
|
-
tableName: z.string().describe("DynamoDB table name"),
|
|
1794
|
-
keyConditionExpression: z
|
|
1795
|
-
.string()
|
|
1796
|
-
.describe("Key condition (e.g., 'pk = :pk')"),
|
|
1797
|
-
expressionAttributeValues: z
|
|
1798
|
-
.string()
|
|
1799
|
-
.describe("JSON object of attribute values"),
|
|
1800
|
-
indexName: z.string().optional().describe("GSI or LSI name to query"),
|
|
1801
|
-
filterExpression: z
|
|
1802
|
-
.string()
|
|
1803
|
-
.optional()
|
|
1804
|
-
.describe("Additional filter expression"),
|
|
1805
|
-
limit: z.number().optional().describe("Max items to return"),
|
|
1806
|
-
scanIndexForward: z
|
|
1807
|
-
.boolean()
|
|
1808
|
-
.optional()
|
|
1809
|
-
.describe("Sort ascending (true) or descending (false)"),
|
|
1810
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1811
|
-
region: z.string().optional().describe("AWS region"),
|
|
1812
|
-
}, async ({ tableName, keyConditionExpression, expressionAttributeValues, indexName, filterExpression, limit, scanIndexForward, profile, region, }) => {
|
|
1813
|
-
log.info("Tool called: aws_dynamodb_query");
|
|
1814
|
-
const result = await queryDynamoDB({
|
|
1815
|
-
tableName,
|
|
1816
|
-
keyConditionExpression,
|
|
1817
|
-
expressionAttributeValues,
|
|
1818
|
-
indexName,
|
|
1819
|
-
filterExpression,
|
|
1820
|
-
limit,
|
|
1821
|
-
scanIndexForward,
|
|
1822
|
-
profile,
|
|
1823
|
-
region,
|
|
1824
|
-
}, log);
|
|
1825
|
-
if (!result.success) {
|
|
1826
|
-
return {
|
|
1827
|
-
content: [
|
|
1828
|
-
{
|
|
1829
|
-
type: "text",
|
|
1830
|
-
text: `Error: ${result.error}`,
|
|
1831
|
-
},
|
|
1832
|
-
],
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
const items = result.data?.Items || [];
|
|
1836
|
-
if (items.length === 0) {
|
|
1837
|
-
return {
|
|
1838
|
-
content: [
|
|
1839
|
-
{
|
|
1840
|
-
type: "text",
|
|
1841
|
-
text: `No items found matching the query.`,
|
|
1842
|
-
},
|
|
1843
|
-
],
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
return {
|
|
1847
|
-
content: [
|
|
1848
|
-
{
|
|
1849
|
-
type: "text",
|
|
1850
|
-
text: [
|
|
1851
|
-
`Found ${items.length} items:`,
|
|
1852
|
-
"",
|
|
1853
|
-
JSON.stringify(items, null, 2),
|
|
1854
|
-
].join("\n"),
|
|
1855
|
-
},
|
|
1856
|
-
],
|
|
1857
|
-
};
|
|
1858
|
-
});
|
|
1859
|
-
log.info("Registered tool: aws_dynamodb_query");
|
|
1860
|
-
// DynamoDB: Get Item
|
|
1861
|
-
server.tool("aws_dynamodb_get_item", "Get a single item from a DynamoDB table by its primary key.", {
|
|
1862
|
-
tableName: z.string().describe("DynamoDB table name"),
|
|
1863
|
-
key: z
|
|
1864
|
-
.string()
|
|
1865
|
-
.describe('JSON object of the primary key (e.g., \'{\\"pk\\":{\\"S\\":\\"user#123\\"},\\"sk\\":{\\"S\\":\\"profile\\"}}\')'),
|
|
1866
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1867
|
-
region: z.string().optional().describe("AWS region"),
|
|
1868
|
-
}, async ({ tableName, key, profile, region }) => {
|
|
1869
|
-
log.info("Tool called: aws_dynamodb_get_item");
|
|
1870
|
-
const result = await getDynamoDBItem({
|
|
1871
|
-
tableName,
|
|
1872
|
-
key,
|
|
1873
|
-
profile,
|
|
1874
|
-
region,
|
|
1875
|
-
}, log);
|
|
1876
|
-
if (!result.success) {
|
|
1877
|
-
return {
|
|
1878
|
-
content: [
|
|
1879
|
-
{
|
|
1880
|
-
type: "text",
|
|
1881
|
-
text: `Error: ${result.error}`,
|
|
1882
|
-
},
|
|
1883
|
-
],
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
if (!result.data?.Item) {
|
|
1887
|
-
return {
|
|
1888
|
-
content: [
|
|
1889
|
-
{
|
|
1890
|
-
type: "text",
|
|
1891
|
-
text: `Item not found with the specified key.`,
|
|
1892
|
-
},
|
|
1893
|
-
],
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
return {
|
|
1897
|
-
content: [
|
|
1898
|
-
{
|
|
1899
|
-
type: "text",
|
|
1900
|
-
text: JSON.stringify(result.data.Item, null, 2),
|
|
1901
|
-
},
|
|
1902
|
-
],
|
|
1903
|
-
};
|
|
1904
|
-
});
|
|
1905
|
-
log.info("Registered tool: aws_dynamodb_get_item");
|
|
1906
|
-
// SQS: List Queues
|
|
1907
|
-
server.tool("aws_sqs_list_queues", "List SQS queues in the account. Filter by queue name prefix.", {
|
|
1908
|
-
queueNamePrefix: z
|
|
1909
|
-
.string()
|
|
1910
|
-
.optional()
|
|
1911
|
-
.describe("Filter by queue name prefix"),
|
|
1912
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1913
|
-
region: z.string().optional().describe("AWS region"),
|
|
1914
|
-
}, async ({ queueNamePrefix, profile, region }) => {
|
|
1915
|
-
log.info("Tool called: aws_sqs_list_queues");
|
|
1916
|
-
const result = await listSQSQueues({
|
|
1917
|
-
queueNamePrefix,
|
|
1918
|
-
profile,
|
|
1919
|
-
region,
|
|
1920
|
-
}, log);
|
|
1921
|
-
if (!result.success) {
|
|
1922
|
-
return {
|
|
1923
|
-
content: [
|
|
1924
|
-
{
|
|
1925
|
-
type: "text",
|
|
1926
|
-
text: `Error: ${result.error}`,
|
|
1927
|
-
},
|
|
1928
|
-
],
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
const queues = result.data?.QueueUrls || [];
|
|
1932
|
-
if (queues.length === 0) {
|
|
1933
|
-
return {
|
|
1934
|
-
content: [
|
|
1935
|
-
{
|
|
1936
|
-
type: "text",
|
|
1937
|
-
text: queueNamePrefix
|
|
1938
|
-
? `No queues found with prefix "${queueNamePrefix}".`
|
|
1939
|
-
: "No SQS queues found in this account/region.",
|
|
1940
|
-
},
|
|
1941
|
-
],
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
const formatted = queues.map((url) => `- ${url}`).join("\n");
|
|
1945
|
-
return {
|
|
1946
|
-
content: [
|
|
1947
|
-
{
|
|
1948
|
-
type: "text",
|
|
1949
|
-
text: [
|
|
1950
|
-
`Found ${queues.length} queues:`,
|
|
1951
|
-
"",
|
|
1952
|
-
formatted,
|
|
1953
|
-
"",
|
|
1954
|
-
"Use aws_sqs_get_queue_attributes for details on a specific queue.",
|
|
1955
|
-
].join("\n"),
|
|
1956
|
-
},
|
|
1957
|
-
],
|
|
1958
|
-
};
|
|
1959
|
-
});
|
|
1960
|
-
log.info("Registered tool: aws_sqs_list_queues");
|
|
1961
|
-
// SQS: Get Queue Attributes
|
|
1962
|
-
server.tool("aws_sqs_get_queue_attributes", "Get attributes for an SQS queue including approximate message count, visibility timeout, and dead-letter config.", {
|
|
1963
|
-
queueUrl: z.string().describe("SQS queue URL"),
|
|
1964
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
1965
|
-
region: z.string().optional().describe("AWS region"),
|
|
1966
|
-
}, async ({ queueUrl, profile, region }) => {
|
|
1967
|
-
log.info("Tool called: aws_sqs_get_queue_attributes");
|
|
1968
|
-
const result = await getSQSQueueAttributes({
|
|
1969
|
-
queueUrl,
|
|
1970
|
-
profile,
|
|
1971
|
-
region,
|
|
1972
|
-
}, log);
|
|
1973
|
-
if (!result.success) {
|
|
1974
|
-
return {
|
|
1975
|
-
content: [
|
|
1976
|
-
{
|
|
1977
|
-
type: "text",
|
|
1978
|
-
text: `Error: ${result.error}`,
|
|
1979
|
-
},
|
|
1980
|
-
],
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
const attrs = result.data?.Attributes;
|
|
1984
|
-
if (!attrs) {
|
|
1985
|
-
return {
|
|
1986
|
-
content: [
|
|
1987
|
-
{
|
|
1988
|
-
type: "text",
|
|
1989
|
-
text: `No attributes found for queue.`,
|
|
1990
|
-
},
|
|
1991
|
-
],
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
const formatted = Object.entries(attrs)
|
|
1995
|
-
.map(([key, value]) => `- ${key}: ${value}`)
|
|
1996
|
-
.join("\n");
|
|
1997
|
-
return {
|
|
1998
|
-
content: [
|
|
1999
|
-
{
|
|
2000
|
-
type: "text",
|
|
2001
|
-
text: [`Queue Attributes:`, "", formatted].join("\n"),
|
|
2002
|
-
},
|
|
2003
|
-
],
|
|
2004
|
-
};
|
|
2005
|
-
});
|
|
2006
|
-
log.info("Registered tool: aws_sqs_get_queue_attributes");
|
|
2007
|
-
// SQS: Receive Message
|
|
2008
|
-
server.tool("aws_sqs_receive_message", "Receive messages from an SQS queue for inspection. Messages are returned to the queue after visibility timeout.", {
|
|
2009
|
-
queueUrl: z.string().describe("SQS queue URL"),
|
|
2010
|
-
maxNumberOfMessages: z
|
|
2011
|
-
.number()
|
|
2012
|
-
.optional()
|
|
2013
|
-
.describe("Max messages to receive (1-10, default 1)"),
|
|
2014
|
-
visibilityTimeout: z
|
|
2015
|
-
.number()
|
|
2016
|
-
.optional()
|
|
2017
|
-
.describe("Seconds to hide message (default 30)"),
|
|
2018
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
2019
|
-
region: z.string().optional().describe("AWS region"),
|
|
2020
|
-
}, async ({ queueUrl, maxNumberOfMessages, visibilityTimeout, profile, region, }) => {
|
|
2021
|
-
log.info("Tool called: aws_sqs_receive_message");
|
|
2022
|
-
const result = await receiveSQSMessage({
|
|
2023
|
-
queueUrl,
|
|
2024
|
-
maxNumberOfMessages: maxNumberOfMessages || 1,
|
|
2025
|
-
visibilityTimeout: visibilityTimeout || 30,
|
|
2026
|
-
profile,
|
|
2027
|
-
region,
|
|
2028
|
-
}, log);
|
|
2029
|
-
if (!result.success) {
|
|
2030
|
-
return {
|
|
2031
|
-
content: [
|
|
2032
|
-
{
|
|
2033
|
-
type: "text",
|
|
2034
|
-
text: `Error: ${result.error}`,
|
|
2035
|
-
},
|
|
2036
|
-
],
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
const messages = result.data?.Messages || [];
|
|
2040
|
-
if (messages.length === 0) {
|
|
2041
|
-
return {
|
|
2042
|
-
content: [
|
|
2043
|
-
{
|
|
2044
|
-
type: "text",
|
|
2045
|
-
text: `No messages available in the queue.`,
|
|
2046
|
-
},
|
|
2047
|
-
],
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
const formatted = messages
|
|
2051
|
-
.map((m, i) => {
|
|
2052
|
-
return [
|
|
2053
|
-
`Message ${i + 1}:`,
|
|
2054
|
-
` ID: ${m.MessageId}`,
|
|
2055
|
-
` Body: ${m.Body}`,
|
|
2056
|
-
m.Attributes
|
|
2057
|
-
? ` Attributes: ${JSON.stringify(m.Attributes)}`
|
|
2058
|
-
: null,
|
|
2059
|
-
]
|
|
2060
|
-
.filter(Boolean)
|
|
2061
|
-
.join("\n");
|
|
2062
|
-
})
|
|
2063
|
-
.join("\n\n");
|
|
2064
|
-
return {
|
|
2065
|
-
content: [
|
|
2066
|
-
{
|
|
2067
|
-
type: "text",
|
|
2068
|
-
text: [
|
|
2069
|
-
`Received ${messages.length} messages (will be returned to queue after visibility timeout):`,
|
|
2070
|
-
"",
|
|
2071
|
-
formatted,
|
|
2072
|
-
].join("\n"),
|
|
2073
|
-
},
|
|
2074
|
-
],
|
|
2075
|
-
};
|
|
2076
|
-
});
|
|
2077
|
-
log.info("Registered tool: aws_sqs_receive_message");
|
|
2078
|
-
// SQS: Purge Queue
|
|
2079
|
-
server.tool("aws_sqs_purge_queue", "Delete all messages from an SQS queue. Use with caution - this is irreversible.", {
|
|
2080
|
-
queueUrl: z.string().describe("SQS queue URL"),
|
|
2081
|
-
profile: z.string().optional().describe("AWS profile to use"),
|
|
2082
|
-
region: z.string().optional().describe("AWS region"),
|
|
2083
|
-
}, async ({ queueUrl, profile, region }) => {
|
|
2084
|
-
log.info("Tool called: aws_sqs_purge_queue");
|
|
2085
|
-
const result = await purgeSQSQueue({
|
|
2086
|
-
queueUrl,
|
|
2087
|
-
profile,
|
|
2088
|
-
region,
|
|
2089
|
-
}, log);
|
|
2090
|
-
if (!result.success) {
|
|
2091
|
-
return {
|
|
2092
|
-
content: [
|
|
2093
|
-
{
|
|
2094
|
-
type: "text",
|
|
2095
|
-
text: `Error: ${result.error}`,
|
|
2096
|
-
},
|
|
2097
|
-
],
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2100
|
-
return {
|
|
2101
|
-
content: [
|
|
2102
|
-
{
|
|
2103
|
-
type: "text",
|
|
2104
|
-
text: `Queue purged successfully. All messages have been deleted.`,
|
|
2105
|
-
},
|
|
2106
|
-
],
|
|
2107
|
-
};
|
|
2108
|
-
});
|
|
2109
|
-
log.info("Registered tool: aws_sqs_purge_queue");
|
|
2110
|
-
log.info("MCP server configuration complete");
|
|
41
|
+
if (verbose) {
|
|
42
|
+
console.error(`[jaypie-mcp] Registered ${suite.services.length} tools from suite`);
|
|
43
|
+
console.error(`[jaypie-mcp] Categories: ${suite.categories.join(", ")}`);
|
|
44
|
+
}
|
|
2111
45
|
return server;
|
|
2112
46
|
}
|
|
2113
47
|
|