@pagopa/dx-mcpserver 0.0.12 → 0.1.1

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.
Files changed (45) hide show
  1. package/README.md +80 -35
  2. package/dist/__tests__/__mocks__/handlers.js +48 -0
  3. package/dist/__tests__/http-endpoints.test.js +246 -0
  4. package/dist/__tests__/index.test.js +125 -0
  5. package/dist/__tests__/session.test.js +66 -0
  6. package/dist/cli.js +12 -0
  7. package/dist/config/__tests__/aws-config.test.js +15 -0
  8. package/dist/config/__tests__/constants.test.js +31 -0
  9. package/dist/config/aws.js +14 -17
  10. package/dist/config/constants.js +9 -0
  11. package/dist/config/logging.js +6 -10
  12. package/dist/config/monitoring.js +13 -5
  13. package/dist/config.js +67 -0
  14. package/dist/decorators/{promptUsageMonitoring.js → prompt-usage-monitoring.js} +8 -11
  15. package/dist/decorators/{toolUsageMonitoring.js → tool-usage-monitoring.js} +16 -16
  16. package/dist/handlers/ask.js +54 -0
  17. package/dist/handlers/search.js +60 -0
  18. package/dist/index.js +17 -59
  19. package/dist/mcp/server.js +88 -0
  20. package/dist/server.js +109 -0
  21. package/dist/services/__tests__/bedrock-retrieve-and-generate.test.js +116 -0
  22. package/dist/services/__tests__/bedrock.test.js +160 -1
  23. package/dist/services/bedrock-retrieve-and-generate.js +55 -0
  24. package/dist/services/bedrock.js +56 -0
  25. package/dist/session.js +12 -0
  26. package/dist/tools/__tests__/QueryValidation.test.js +83 -0
  27. package/dist/tools/__tests__/query-pago-pa-dx-documentation.test.js +47 -0
  28. package/dist/tools/__tests__/registry.test.js +81 -0
  29. package/dist/tools/query-pagopa-dx-documentation.js +122 -0
  30. package/dist/tools/registry.js +20 -0
  31. package/dist/types.js +1 -0
  32. package/dist/utils/__tests__/error-handling.test.js +168 -0
  33. package/dist/utils/error-handling.js +74 -0
  34. package/dist/utils/errors.js +12 -0
  35. package/dist/utils/filter-undefined.js +6 -0
  36. package/dist/utils/http.js +36 -0
  37. package/dist/utils/normalize-boolean.js +13 -0
  38. package/package.json +7 -7
  39. package/dist/auth/__tests__/githubAuth.test.js +0 -65
  40. package/dist/auth/github.js +0 -38
  41. package/dist/config/__tests__/awsConfig.test.js +0 -17
  42. package/dist/tools/QueryPagoPADXDocumentation.js +0 -35
  43. package/dist/tools/SearchGitHubCode.js +0 -84
  44. package/dist/tools/__tests__/QueryPagoPADXDocumentation.test.js +0 -22
  45. /package/dist/services/__tests__/{resolveToWebsiteUrl.test.js → resolve-to-website-url.test.js} +0 -0
@@ -0,0 +1,81 @@
1
+ import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { createToolDefinitions } from "../registry.js";
4
+ const toolDefinitions = createToolDefinitions({
5
+ aws: {
6
+ knowledgeBaseId: "kb-id",
7
+ modelArn: "arn:aws:bedrock:eu-central-1:123456789012:model/model-id",
8
+ region: "eu-central-1",
9
+ rerankingEnabled: false,
10
+ },
11
+ githubSearchOrg: "pagopa",
12
+ kbRuntimeClient: new BedrockAgentRuntimeClient({ region: "eu-central-1" }),
13
+ });
14
+ describe("Tool Registry", () => {
15
+ describe("registry structure", () => {
16
+ it("should contain expected tools", () => {
17
+ expect(toolDefinitions).toHaveLength(1);
18
+ const toolIds = toolDefinitions.map((entry) => entry.id);
19
+ expect(toolIds).toContain("pagopa_query_documentation");
20
+ });
21
+ it("should have unique tool IDs", () => {
22
+ const toolIds = toolDefinitions.map((entry) => entry.id);
23
+ const uniqueIds = new Set(toolIds);
24
+ expect(uniqueIds.size).toBe(toolIds.length);
25
+ });
26
+ it("should follow snake_case naming convention with pagopa prefix", () => {
27
+ for (const entry of toolDefinitions) {
28
+ expect(entry.id).toMatch(/^pagopa_[a-z_]+$/);
29
+ }
30
+ });
31
+ });
32
+ describe("tool entries", () => {
33
+ it("should have valid tool definitions", () => {
34
+ for (const entry of toolDefinitions) {
35
+ expect(entry.tool).toBeDefined();
36
+ expect(entry.tool.name).toBeTruthy();
37
+ expect(entry.tool.description).toBeTruthy();
38
+ expect(entry.tool.execute).toBeTypeOf("function");
39
+ expect(entry.tool.parameters).toBeDefined();
40
+ }
41
+ });
42
+ it("should have proper annotations", () => {
43
+ for (const entry of toolDefinitions) {
44
+ expect(entry.tool.annotations).toBeDefined();
45
+ expect(entry.tool.annotations.title).toBeTruthy();
46
+ }
47
+ });
48
+ it("should have boolean values for hint annotations when defined", () => {
49
+ for (const entry of toolDefinitions) {
50
+ const { annotations } = entry.tool;
51
+ if (annotations.readOnlyHint !== undefined) {
52
+ expect(annotations.readOnlyHint).toBeTypeOf("boolean");
53
+ }
54
+ if (annotations.destructiveHint !== undefined) {
55
+ expect(annotations.destructiveHint).toBeTypeOf("boolean");
56
+ }
57
+ if (annotations.idempotentHint !== undefined) {
58
+ expect(annotations.idempotentHint).toBeTypeOf("boolean");
59
+ }
60
+ if (annotations.openWorldHint !== undefined) {
61
+ expect(annotations.openWorldHint).toBeTypeOf("boolean");
62
+ }
63
+ }
64
+ });
65
+ });
66
+ describe("session requirements", () => {
67
+ it("should mark documentation tool as not requiring session", () => {
68
+ const docTool = toolDefinitions.find((entry) => entry.id === "pagopa_query_documentation");
69
+ expect(docTool?.requiresSession).toBe(false);
70
+ });
71
+ });
72
+ describe("pagopa_query_documentation", () => {
73
+ const docTool = toolDefinitions.find((entry) => entry.id === "pagopa_query_documentation");
74
+ it("should have correct metadata", () => {
75
+ expect(docTool?.tool.name).toBe("pagopa_query_documentation");
76
+ expect(docTool?.tool.annotations.readOnlyHint).toBe(true);
77
+ expect(docTool?.tool.annotations.destructiveHint).toBe(false);
78
+ expect(docTool?.tool.annotations.idempotentHint).toBe(true);
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ import { queryKnowledgeBase } from "../services/bedrock.js";
3
+ import { handleApiError } from "../utils/error-handling.js";
4
+ /**
5
+ * Response format options for documentation queries
6
+ */
7
+ var ResponseFormat;
8
+ (function (ResponseFormat) {
9
+ ResponseFormat["JSON"] = "json";
10
+ ResponseFormat["MARKDOWN"] = "markdown";
11
+ })(ResponseFormat || (ResponseFormat = {}));
12
+ /**
13
+ * Pagination constants
14
+ */
15
+ const DEFAULT_NUMBER_OF_RESULTS = 5;
16
+ const MIN_RESULTS = 1;
17
+ const MAX_RESULTS = 20;
18
+ /**
19
+ * Zod schema for QueryPagoPADXDocumentation input validation
20
+ */
21
+ export const QueryPagoPADXDocumentationInputSchema = z
22
+ .object({
23
+ format: z
24
+ .nativeEnum(ResponseFormat)
25
+ .default(ResponseFormat.MARKDOWN)
26
+ .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
27
+ number_of_results: z
28
+ .number()
29
+ .int()
30
+ .min(MIN_RESULTS, `Must return at least ${MIN_RESULTS} result`)
31
+ .max(MAX_RESULTS, `Cannot exceed ${MAX_RESULTS} results`)
32
+ .default(DEFAULT_NUMBER_OF_RESULTS)
33
+ .describe(`Number of documentation chunks to retrieve (${MIN_RESULTS}-${MAX_RESULTS}, default: ${DEFAULT_NUMBER_OF_RESULTS}). Use more for comprehensive answers, fewer for quick lookups.`),
34
+ query: z
35
+ .string()
36
+ .min(3, "Query must be at least 3 characters")
37
+ .max(500, "Query must not exceed 500 characters")
38
+ .describe("A natural language query in English used to search the DX documentation for relevant information."),
39
+ })
40
+ .strict();
41
+ /**
42
+ * A tool that provides access to the complete PagoPA DX documentation.
43
+ * It uses a Bedrock knowledge base to answer queries about DX tools, patterns, and best practices.
44
+ */
45
+ export function createQueryPagoPADXDocumentationTool(config) {
46
+ return {
47
+ annotations: {
48
+ destructiveHint: false,
49
+ idempotentHint: true,
50
+ openWorldHint: false,
51
+ readOnlyHint: true,
52
+ title: "Query PagoPA DX Documentation",
53
+ },
54
+ description: `Query the PagoPA DX documentation knowledge base for information about developer tools, patterns, and best practices.
55
+
56
+ This tool provides access to the complete PagoPA DX documentation covering:
57
+ - Getting started, monorepo setup, dev containers, and GitHub collaboration
58
+ - Git workflows and pull requests
59
+ - DX pipelines setup and management
60
+ - TypeScript development (npm scripts, ESLint, code review)
61
+ - Terraform (folder structure, DX modules, Azure provider, pre-commit hooks, validation, deployment, drift detection)
62
+ - Azure development (naming conventions, policies, IAM, API Management, monitoring, networking, deployments, static websites, Service Bus, data archiving)
63
+ - Container development (Docker images)
64
+ - Contributing to DX (Azure provider, Terraform modules, documentation)
65
+
66
+ Args:
67
+ - query (string, required): Natural language query in English (3-500 characters)
68
+ - format ('markdown' | 'json', optional): Output format (default: 'markdown')
69
+ - number_of_results (number, optional): Number of documentation chunks to retrieve (1-20, default: 5)
70
+
71
+ Returns:
72
+ For JSON format:
73
+ {
74
+ "query": string, // The original query
75
+ "result": string, // Documentation content matching the query
76
+ "number_of_results": number // How many chunks were requested
77
+ }
78
+
79
+ For Markdown format:
80
+ Human-readable documentation content with proper formatting.
81
+
82
+ Examples:
83
+ - "How do I set up a new Terraform module?" -> Returns step-by-step guide
84
+ - "What are the Azure naming conventions?" -> Returns naming standards
85
+ - "How to configure ESLint for TypeScript?" -> Returns configuration guide
86
+
87
+ Notes:
88
+ - All queries should be written in English
89
+ - Use \`number_of_results: 1-3\` for quick lookups, \`10-20\` for comprehensive research
90
+ - For Terraform module examples and code patterns, use the \`search_code\` tool from GitHub's MCP server
91
+
92
+ Error Handling:
93
+ - Returns "Error: Query must be at least 3 characters" for queries too short
94
+ - Returns "Error: Query must not exceed 500 characters" for queries too long
95
+ - Returns "Error: ..." for API or network errors`,
96
+ execute: async (args) => {
97
+ const parsedArgsResult = QueryPagoPADXDocumentationInputSchema.safeParse(args);
98
+ if (!parsedArgsResult.success) {
99
+ return handleApiError(parsedArgsResult.error);
100
+ }
101
+ const parsedArgs = parsedArgsResult.data;
102
+ const numberOfResults = parsedArgs.number_of_results;
103
+ try {
104
+ const result = await queryKnowledgeBase(config.knowledgeBaseId, parsedArgs.query, config.kbRuntimeClient, numberOfResults, config.rerankingEnabled);
105
+ const format = parsedArgs.format;
106
+ if (format === ResponseFormat.JSON) {
107
+ return JSON.stringify({
108
+ number_of_results: numberOfResults,
109
+ query: parsedArgs.query,
110
+ result,
111
+ }, null, 2);
112
+ }
113
+ return result;
114
+ }
115
+ catch (error) {
116
+ return handleApiError(error);
117
+ }
118
+ },
119
+ name: "pagopa_query_documentation",
120
+ parameters: QueryPagoPADXDocumentationInputSchema,
121
+ };
122
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tool registry for dynamic tool registration
3
+ *
4
+ * This file provides a centralized, scalable way to manage MCP tools.
5
+ * Adding a new tool only requires adding it to the createToolDefinitions return array.
6
+ */
7
+ import { createQueryPagoPADXDocumentationTool } from "./query-pagopa-dx-documentation.js";
8
+ export function createToolDefinitions(config) {
9
+ return [
10
+ {
11
+ id: "pagopa_query_documentation",
12
+ requiresSession: false,
13
+ tool: createQueryPagoPADXDocumentationTool({
14
+ kbRuntimeClient: config.kbRuntimeClient,
15
+ knowledgeBaseId: config.aws.knowledgeBaseId,
16
+ rerankingEnabled: config.aws.rerankingEnabled,
17
+ }),
18
+ },
19
+ ];
20
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,168 @@
1
+ import { AxiosError, AxiosHeaders } from "axios";
2
+ import { describe, expect, it } from "vitest";
3
+ import { ZodError } from "zod";
4
+ import { handleApiError, isAxiosError, isZodError } from "../error-handling.js";
5
+ /**
6
+ * Helper to create an AxiosError with a response
7
+ */
8
+ const createAxiosResponseError = (status, statusText) => {
9
+ const config = { headers: new AxiosHeaders() };
10
+ return new AxiosError(statusText, "ERR_BAD_REQUEST", config, undefined, {
11
+ config,
12
+ data: {},
13
+ headers: {},
14
+ status,
15
+ statusText,
16
+ });
17
+ };
18
+ describe("handleApiError - Axios HTTP status codes", () => {
19
+ it("should handle 400 Bad Request", () => {
20
+ const error = createAxiosResponseError(400, "Bad Request");
21
+ expect(handleApiError(error)).toBe("Error: Bad request. Please check your input parameters.");
22
+ });
23
+ it("should handle 401 Unauthorized", () => {
24
+ const error = createAxiosResponseError(401, "Unauthorized");
25
+ expect(handleApiError(error)).toBe("Error: Authentication failed. Please check your credentials.");
26
+ });
27
+ it("should handle 403 Forbidden", () => {
28
+ const error = createAxiosResponseError(403, "Forbidden");
29
+ expect(handleApiError(error)).toBe("Error: Permission denied. You don't have access to this resource.");
30
+ });
31
+ it("should handle 404 Not Found", () => {
32
+ const error = createAxiosResponseError(404, "Not Found");
33
+ expect(handleApiError(error)).toBe("Error: Resource not found. Please check the ID is correct.");
34
+ });
35
+ it("should handle 429 Rate Limit", () => {
36
+ const error = createAxiosResponseError(429, "Too Many Requests");
37
+ expect(handleApiError(error)).toBe("Error: Rate limit exceeded. Please wait before making more requests.");
38
+ });
39
+ it("should handle 500 Internal Server Error", () => {
40
+ const error = createAxiosResponseError(500, "Internal Server Error");
41
+ expect(handleApiError(error)).toBe("Error: Server error. The service is temporarily unavailable.");
42
+ });
43
+ it("should handle 502 Bad Gateway", () => {
44
+ const error = createAxiosResponseError(502, "Bad Gateway");
45
+ expect(handleApiError(error)).toBe("Error: Bad gateway. The service is temporarily unavailable.");
46
+ });
47
+ it("should handle 503 Service Unavailable", () => {
48
+ const error = createAxiosResponseError(503, "Service Unavailable");
49
+ expect(handleApiError(error)).toBe("Error: Service unavailable. Please try again later.");
50
+ });
51
+ it("should handle unknown status codes", () => {
52
+ const error = createAxiosResponseError(418, "I'm a teapot");
53
+ expect(handleApiError(error)).toBe("Error: API request failed with status 418");
54
+ });
55
+ });
56
+ describe("handleApiError - Axios network errors", () => {
57
+ it("should handle ECONNABORTED timeout errors", () => {
58
+ const error = new AxiosError("Timeout", "ECONNABORTED");
59
+ expect(handleApiError(error)).toBe("Error: Request timed out. Please try again.");
60
+ });
61
+ it("should handle ENOTFOUND errors", () => {
62
+ const error = new AxiosError("Not Found", "ENOTFOUND");
63
+ expect(handleApiError(error)).toBe("Error: Could not connect to the server. Please check your network.");
64
+ });
65
+ it("should handle ECONNREFUSED errors", () => {
66
+ const error = new AxiosError("Connection Refused", "ECONNREFUSED");
67
+ expect(handleApiError(error)).toBe("Error: Connection refused. The server may be down.");
68
+ });
69
+ it("should handle other network errors", () => {
70
+ const error = new AxiosError("Unknown network error");
71
+ expect(handleApiError(error)).toBe("Error: Network error occurred: Unknown network error");
72
+ });
73
+ });
74
+ describe("handleApiError - Zod validation errors", () => {
75
+ it("should format single validation error", () => {
76
+ const error = new ZodError([
77
+ {
78
+ code: "too_small",
79
+ inclusive: true,
80
+ message: "String must contain at least 3 character(s)",
81
+ minimum: 3,
82
+ path: ["query"],
83
+ type: "string",
84
+ },
85
+ ]);
86
+ expect(handleApiError(error)).toBe("Error: Invalid input - query: String must contain at least 3 character(s)");
87
+ });
88
+ it("should format multiple validation errors", () => {
89
+ const error = new ZodError([
90
+ {
91
+ code: "too_small",
92
+ inclusive: true,
93
+ message: "String must contain at least 3 character(s)",
94
+ minimum: 3,
95
+ path: ["query"],
96
+ type: "string",
97
+ },
98
+ {
99
+ code: "invalid_type",
100
+ expected: "number",
101
+ message: "Expected number, received string",
102
+ path: ["page"],
103
+ received: "string",
104
+ },
105
+ ]);
106
+ expect(handleApiError(error)).toBe("Error: Invalid input - query: String must contain at least 3 character(s); page: Expected number, received string");
107
+ });
108
+ it("should handle nested path errors", () => {
109
+ const error = new ZodError([
110
+ {
111
+ code: "invalid_type",
112
+ expected: "string",
113
+ message: "Required",
114
+ path: ["data", "nested", "field"],
115
+ received: "undefined",
116
+ },
117
+ ]);
118
+ expect(handleApiError(error)).toBe("Error: Invalid input - data.nested.field: Required");
119
+ });
120
+ });
121
+ describe("handleApiError - generic errors", () => {
122
+ it("should handle Error instances", () => {
123
+ const error = new Error("Something went wrong");
124
+ expect(handleApiError(error)).toBe("Error: Something went wrong");
125
+ });
126
+ it("should handle string errors", () => {
127
+ expect(handleApiError("Raw string error")).toBe("Error: Unexpected error occurred: Raw string error");
128
+ });
129
+ it("should handle null", () => {
130
+ expect(handleApiError(null)).toBe("Error: Unexpected error occurred: null");
131
+ });
132
+ it("should handle undefined", () => {
133
+ expect(handleApiError(undefined)).toBe("Error: Unexpected error occurred: undefined");
134
+ });
135
+ it("should handle objects", () => {
136
+ expect(handleApiError({ custom: "error" })).toBe("Error: Unexpected error occurred: [object Object]");
137
+ });
138
+ });
139
+ describe("isAxiosError", () => {
140
+ it("should return true for AxiosError instances", () => {
141
+ const error = new AxiosError("Test error");
142
+ expect(isAxiosError(error)).toBe(true);
143
+ });
144
+ it("should return false for regular Error instances", () => {
145
+ const error = new Error("Test error");
146
+ expect(isAxiosError(error)).toBe(false);
147
+ });
148
+ it("should return false for non-errors", () => {
149
+ expect(isAxiosError("string")).toBe(false);
150
+ expect(isAxiosError(null)).toBe(false);
151
+ expect(isAxiosError(undefined)).toBe(false);
152
+ });
153
+ });
154
+ describe("isZodError", () => {
155
+ it("should return true for ZodError instances", () => {
156
+ const error = new ZodError([]);
157
+ expect(isZodError(error)).toBe(true);
158
+ });
159
+ it("should return false for regular Error instances", () => {
160
+ const error = new Error("Test error");
161
+ expect(isZodError(error)).toBe(false);
162
+ });
163
+ it("should return false for non-errors", () => {
164
+ expect(isZodError("string")).toBe(false);
165
+ expect(isZodError(null)).toBe(false);
166
+ expect(isZodError(undefined)).toBe(false);
167
+ });
168
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Centralized error handling utilities for the MCP server.
3
+ * Provides consistent error messages across all tools.
4
+ */
5
+ import { AxiosError } from "axios";
6
+ import { ZodError } from "zod";
7
+ /**
8
+ * Handles API errors and returns user-friendly error messages.
9
+ * Supports Axios errors, Zod validation errors, and generic errors.
10
+ *
11
+ * @param error - The error to handle
12
+ * @returns A formatted error message string
13
+ */
14
+ export function handleApiError(error) {
15
+ // Handle Axios HTTP errors
16
+ if (error instanceof AxiosError) {
17
+ if (error.response) {
18
+ switch (error.response.status) {
19
+ case 400:
20
+ return "Error: Bad request. Please check your input parameters.";
21
+ case 401:
22
+ return "Error: Authentication failed. Please check your credentials.";
23
+ case 403:
24
+ return "Error: Permission denied. You don't have access to this resource.";
25
+ case 404:
26
+ return "Error: Resource not found. Please check the ID is correct.";
27
+ case 429:
28
+ return "Error: Rate limit exceeded. Please wait before making more requests.";
29
+ case 500:
30
+ return "Error: Server error. The service is temporarily unavailable.";
31
+ case 502:
32
+ return "Error: Bad gateway. The service is temporarily unavailable.";
33
+ case 503:
34
+ return "Error: Service unavailable. Please try again later.";
35
+ default:
36
+ return `Error: API request failed with status ${error.response.status}`;
37
+ }
38
+ }
39
+ else if (error.code === "ECONNABORTED") {
40
+ return "Error: Request timed out. Please try again.";
41
+ }
42
+ else if (error.code === "ENOTFOUND") {
43
+ return "Error: Could not connect to the server. Please check your network.";
44
+ }
45
+ else if (error.code === "ECONNREFUSED") {
46
+ return "Error: Connection refused. The server may be down.";
47
+ }
48
+ return `Error: Network error occurred: ${error.message}`;
49
+ }
50
+ // Handle Zod validation errors
51
+ if (error instanceof ZodError) {
52
+ const issues = error.issues
53
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
54
+ .join("; ");
55
+ return `Error: Invalid input - ${issues}`;
56
+ }
57
+ // Handle generic errors
58
+ if (error instanceof Error) {
59
+ return `Error: ${error.message}`;
60
+ }
61
+ return `Error: Unexpected error occurred: ${String(error)}`;
62
+ }
63
+ /**
64
+ * Type guard to check if an error is an Axios error
65
+ */
66
+ export function isAxiosError(error) {
67
+ return error instanceof AxiosError;
68
+ }
69
+ /**
70
+ * Type guard to check if an error is a Zod validation error
71
+ */
72
+ export function isZodError(error) {
73
+ return error instanceof ZodError;
74
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Formats a list of Zod issues into a single, human-readable string.
3
+ * This is useful for error messages when configuration or input validation fails.
4
+ *
5
+ * @param issues - The array of Zod issues to format
6
+ * @returns A semicolon-separated string of formatted issues
7
+ */
8
+ export function formatZodIssues(issues) {
9
+ return issues
10
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
11
+ .join("; ");
12
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Filter out undefined values from an object to match emitCustomEvent expectations
3
+ */
4
+ export function filterUndefined(obj) {
5
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
6
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Parses JSON body from an incoming HTTP request.
3
+ */
4
+ export async function parseJsonBody(req) {
5
+ let body = "";
6
+ for await (const chunk of req) {
7
+ body += chunk;
8
+ }
9
+ if (!body)
10
+ return undefined;
11
+ return JSON.parse(body);
12
+ }
13
+ /**
14
+ * Sends an error response in a standard format.
15
+ */
16
+ export function sendErrorResponse(res, statusCode, message, details) {
17
+ sendJsonResponse(res, statusCode, {
18
+ error: message,
19
+ ...(details ? { details } : {}),
20
+ });
21
+ }
22
+ /**
23
+ * Sends a JSON response with the specified status code.
24
+ */
25
+ export function sendJsonResponse(res, statusCode, data) {
26
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify(data));
28
+ }
29
+ /**
30
+ * Sets standard CORS headers for the response.
31
+ */
32
+ export function setCorsHeaders(res) {
33
+ res.setHeader("Access-Control-Allow-Origin", "*");
34
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS, DELETE");
35
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
36
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Normalizes a string or undefined value into a boolean.
3
+ *
4
+ * @param value - The string value to normalize (e.g., from an environment variable)
5
+ * @param defaultValue - The default boolean value to return if the input is undefined or empty
6
+ * @returns The boolean representation of the string, or the default value
7
+ */
8
+ export function normalizeBoolean(value, defaultValue) {
9
+ if (!value) {
10
+ return defaultValue;
11
+ }
12
+ return value.trim().toLowerCase() === "true";
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/dx-mcpserver",
3
- "version": "0.0.12",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "An MCP server that supports developers using DX tools.",
6
6
  "repository": {
@@ -16,17 +16,17 @@
16
16
  "dist"
17
17
  ],
18
18
  "bin": {
19
- "dx": "./dist/index.js"
19
+ "dx": "./dist/cli.js"
20
20
  },
21
21
  "dependencies": {
22
22
  "@aws-sdk/client-bedrock-agent-runtime": "^3.583.0",
23
23
  "@logtape/logtape": "^1.3.4",
24
+ "@modelcontextprotocol/sdk": "^1.25.2",
24
25
  "@octokit/rest": "^22.0.0",
25
26
  "axios": "^1.12.2",
26
- "fastmcp": "^3.19.1",
27
27
  "zod": "^3.25.76",
28
- "@pagopa/azure-tracing": "^0.4.10",
29
- "@pagopa/dx-mcpprompts": "^0.2.0"
28
+ "@pagopa/dx-mcpprompts": "^0.2.2",
29
+ "@pagopa/azure-tracing": "^0.4.11"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.19.1",
@@ -36,10 +36,10 @@
36
36
  "tsx": "^4.20.6",
37
37
  "typescript": "~5.8.3",
38
38
  "vitest": "^3.2.4",
39
- "@pagopa/eslint-config": "^5.1.1"
39
+ "@pagopa/eslint-config": "^5.1.2"
40
40
  },
41
41
  "scripts": {
42
- "start": "tsx src/index.ts",
42
+ "start": "tsx src/cli.ts",
43
43
  "format": "prettier --write .",
44
44
  "format:check": "prettier --check .",
45
45
  "typecheck": "tsc --noEmit",
@@ -1,65 +0,0 @@
1
- import { getLogger } from "@logtape/logtape";
2
- import { Octokit } from "@octokit/rest";
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import * as githubAuth from "../github.js";
5
- vi.mock("@octokit/rest");
6
- describe("verifyGithubUser", () => {
7
- let loggerSpy;
8
- beforeEach(() => {
9
- vi.clearAllMocks();
10
- process.env.REQUIRED_ORGANIZATIONS = "pagopa";
11
- // Get logger and spy on its methods - no need for special configuration
12
- const logger = getLogger(["mcpserver", "github-auth"]);
13
- loggerSpy = {
14
- debug: vi.spyOn(logger, "debug"),
15
- error: vi.spyOn(logger, "error"),
16
- warn: vi.spyOn(logger, "warn"),
17
- };
18
- });
19
- it("returns false if no token is provided", async () => {
20
- const result = await githubAuth.verifyGithubUser("");
21
- expect(result).toBe(false);
22
- });
23
- it("returns false if Octokit throws", async () => {
24
- vi.mocked(Octokit).mockImplementation(() => ({
25
- rest: {
26
- orgs: {
27
- listForAuthenticatedUser: vi
28
- .fn()
29
- .mockRejectedValue(new Error("fail")),
30
- },
31
- },
32
- }));
33
- const result = await githubAuth.verifyGithubUser("token");
34
- expect(result).toBe(false);
35
- expect(loggerSpy.error).toHaveBeenCalledWith("Error verifying GitHub organization membership", { error: expect.any(Error) });
36
- });
37
- it("returns false if user is not member of required org", async () => {
38
- vi.mocked(Octokit).mockImplementation(() => ({
39
- rest: {
40
- orgs: {
41
- listForAuthenticatedUser: vi.fn().mockResolvedValue({
42
- data: [{ login: "otherorg" }],
43
- }),
44
- },
45
- },
46
- }));
47
- const result = await githubAuth.verifyGithubUser("token");
48
- expect(result).toBe(false);
49
- expect(loggerSpy.warn).toHaveBeenCalledWith("User is not a member of any of the required organizations: pagopa");
50
- });
51
- it("returns true if user is member of required org", async () => {
52
- vi.mocked(Octokit).mockImplementation(() => ({
53
- rest: {
54
- orgs: {
55
- listForAuthenticatedUser: vi.fn().mockResolvedValue({
56
- data: [{ login: "pagopa" }],
57
- }),
58
- },
59
- },
60
- }));
61
- const result = await githubAuth.verifyGithubUser("token");
62
- expect(result).toBe(true);
63
- expect(loggerSpy.debug).toHaveBeenCalledWith("User is a member of one of the required organizations: pagopa");
64
- });
65
- });