@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,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { API_TIMEOUT, DEFAULT_PAGE_SIZE, MAX_RESULTS } from "../constants.js";
3
+ describe("Server Constants", () => {
4
+ describe("API_TIMEOUT", () => {
5
+ it("should be a positive number", () => {
6
+ expect(API_TIMEOUT).toBeGreaterThan(0);
7
+ });
8
+ it("should be 30 seconds (30000ms)", () => {
9
+ expect(API_TIMEOUT).toBe(30000);
10
+ });
11
+ });
12
+ describe("MAX_RESULTS", () => {
13
+ it("should be a positive number", () => {
14
+ expect(MAX_RESULTS).toBeGreaterThan(0);
15
+ });
16
+ it("should be 100", () => {
17
+ expect(MAX_RESULTS).toBe(100);
18
+ });
19
+ });
20
+ describe("DEFAULT_PAGE_SIZE", () => {
21
+ it("should be a positive number", () => {
22
+ expect(DEFAULT_PAGE_SIZE).toBeGreaterThan(0);
23
+ });
24
+ it("should be 20", () => {
25
+ expect(DEFAULT_PAGE_SIZE).toBe(20);
26
+ });
27
+ it("should be less than MAX_RESULTS", () => {
28
+ expect(DEFAULT_PAGE_SIZE).toBeLessThan(MAX_RESULTS);
29
+ });
30
+ });
31
+ });
@@ -1,12 +1,10 @@
1
+ /**
2
+ * AWS configuration helpers for the MCP server.
3
+ *
4
+ * This module is intentionally side-effect free to keep initialization
5
+ * controlled by the entrypoint.
6
+ */
1
7
  import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
2
- import { getLogger } from "@logtape/logtape";
3
- const logger = getLogger(["mcpserver", "aws-config"]);
4
- // When true, enables reranking for the Bedrock knowledge base queries.
5
- export const kbRerankingEnabled = (process.env.BEDROCK_KB_RERANKING_ENABLED || "true").trim().toLowerCase() ===
6
- "true";
7
- export const knowledgeBaseId = process.env.BEDROCK_KNOWLEDGE_BASE_ID || "";
8
- logger.debug(`Default reranking enabled: ${kbRerankingEnabled} (from BEDROCK_KB_RERANKING_ENABLED)`);
9
- export const region = process.env.AWS_REGION || "eu-central-1";
10
8
  // List of AWS regions that support reranking
11
9
  export const rerankingSupportedRegions = [
12
10
  "ap-northeast-1",
@@ -15,13 +13,12 @@ export const rerankingSupportedRegions = [
15
13
  "us-east-1",
16
14
  "us-west-2",
17
15
  ];
18
- let kbRuntimeClient;
19
- try {
20
- // Initializes the Bedrock Agent Runtime client with the specified region.
21
- kbRuntimeClient = new BedrockAgentRuntimeClient({ region });
16
+ export function createBedrockRuntimeClient(region, logger) {
17
+ try {
18
+ return new BedrockAgentRuntimeClient({ region });
19
+ }
20
+ catch (error) {
21
+ logger.error("Error getting bedrock agent client", { error });
22
+ throw error;
23
+ }
22
24
  }
23
- catch (e) {
24
- logger.error("Error getting bedrock agent client", { error: e });
25
- process.exit(1);
26
- }
27
- export { kbRuntimeClient };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Server constants and configuration values
3
+ */
4
+ /** Timeout for API requests in milliseconds */
5
+ export const API_TIMEOUT = 30000;
6
+ /** Maximum number of results to return */
7
+ export const MAX_RESULTS = 100;
8
+ /** Default page size for paginated results */
9
+ export const DEFAULT_PAGE_SIZE = 20;
@@ -16,31 +16,27 @@
16
16
  * logger.info("Message");
17
17
  */
18
18
  import { configure, getConsoleSink } from "@logtape/logtape";
19
- import { z } from "zod";
19
+ import * as z from "zod";
20
20
  /**
21
21
  * Zod schema for validating log levels.
22
22
  * Based on LogTape's LogLevel type.
23
23
  */
24
24
  const DEFAULT_LOG_LEVEL = "info";
25
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
26
  const logLevelSchema = z
26
- .enum(["debug", "info", "warning", "error"])
27
+ .enum(["error", "trace", "debug", "info", "warning", "fatal"])
27
28
  .catch(DEFAULT_LOG_LEVEL);
28
- export async function configureLogging() {
29
- const logLevel = logLevelSchema.parse(process.env.LOG_LEVEL);
30
- if (logLevel !== process.env.LOG_LEVEL) {
31
- // Use console.warn for this early logging before LogTape is configured
32
- console.warn(`Invalid log level: ${process.env.LOG_LEVEL}. Using ${DEFAULT_LOG_LEVEL}`);
33
- }
29
+ export async function configureLogging(logLevelEnv) {
34
30
  await configure({
35
31
  loggers: [
36
32
  {
37
33
  category: ["mcpserver"],
38
- lowestLevel: logLevel,
34
+ lowestLevel: logLevelEnv,
39
35
  sinks: ["console"],
40
36
  },
41
37
  {
42
38
  category: ["mcp-prompts"],
43
- lowestLevel: logLevel,
39
+ lowestLevel: logLevelEnv,
44
40
  sinks: ["console"],
45
41
  },
46
42
  {
@@ -12,12 +12,20 @@
12
12
  */
13
13
  import { getLogger } from "@logtape/logtape";
14
14
  import { initAzureMonitor } from "@pagopa/azure-tracing/azure-monitor";
15
- const logger = getLogger(["mcpserver", "monitoring"]);
16
- export function configureAzureMonitoring() {
15
+ export function configureAzureMonitoring(config) {
16
+ const logger = getLogger(["mcpserver", "monitoring"]);
17
17
  try {
18
- // Initialize Azure Monitor using environment variables
19
- // This will read APPLICATIONINSIGHTS_CONNECTION_STRING and APPINSIGHTS_SAMPLING_PERCENTAGE
20
- initAzureMonitor();
18
+ if (!config.connectionString) {
19
+ logger.info("Azure Application Insights connection string not provided. Monitoring disabled.");
20
+ return;
21
+ }
22
+ initAzureMonitor([], {
23
+ azureMonitorExporterOptions: {
24
+ connectionString: config.connectionString,
25
+ },
26
+ enableLiveMetrics: true,
27
+ samplingRatio: config.samplingRatio,
28
+ });
21
29
  logger.info("Azure Application Insights monitoring configured successfully");
22
30
  }
23
31
  catch (error) {
package/dist/config.js ADDED
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { formatZodIssues } from "./utils/errors.js";
3
+ import { normalizeBoolean } from "./utils/normalize-boolean.js";
4
+ export const DEFAULT_PORT = 8080;
5
+ const logLevelSchema = z
6
+ .enum(["debug", "info", "warning", "error"])
7
+ .default("info");
8
+ export const envSchema = z.object({
9
+ APPINSIGHTS_SAMPLING_PERCENTAGE: z.coerce.number().min(0).max(100).optional(),
10
+ APPLICATIONINSIGHTS_CONNECTION_STRING: z.string().optional(),
11
+ AWS_REGION: z.string().optional(),
12
+ BEDROCK_KB_RERANKING_ENABLED: z.string().optional(),
13
+ BEDROCK_KNOWLEDGE_BASE_ID: z.string().optional(),
14
+ BEDROCK_MODEL_ARN: z.string().optional(),
15
+ GITHUB_SEARCH_ORG: z.string().optional(),
16
+ LOG_LEVEL: logLevelSchema,
17
+ PORT: z.coerce.number().int().positive().optional(),
18
+ REQUIRED_ORGANIZATIONS: z.string().optional(),
19
+ });
20
+ /**
21
+ * Loads and validates the application configuration from environment variables.
22
+ *
23
+ * @param env - The environment variables object (usually process.env)
24
+ * @returns The validated application configuration
25
+ * @throws Error if environment variables fail validation
26
+ */
27
+ export function loadConfig(env) {
28
+ const parsedEnv = envSchema.safeParse(env);
29
+ if (!parsedEnv.success) {
30
+ throw new Error(`Invalid environment variables: ${formatZodIssues(parsedEnv.error.issues)}`);
31
+ }
32
+ const rawEnv = parsedEnv.data;
33
+ const port = rawEnv.PORT ?? DEFAULT_PORT;
34
+ const rerankingEnabled = normalizeBoolean(rawEnv.BEDROCK_KB_RERANKING_ENABLED, true);
35
+ return {
36
+ aws: {
37
+ knowledgeBaseId: rawEnv.BEDROCK_KNOWLEDGE_BASE_ID ?? "",
38
+ modelArn: rawEnv.BEDROCK_MODEL_ARN ?? "",
39
+ region: rawEnv.AWS_REGION ?? "eu-central-1",
40
+ rerankingEnabled,
41
+ },
42
+ github: {
43
+ requiredOrganizations: parseRequiredOrganizations(rawEnv.REQUIRED_ORGANIZATIONS),
44
+ searchOrg: rawEnv.GITHUB_SEARCH_ORG ?? "pagopa",
45
+ },
46
+ logLevel: rawEnv.LOG_LEVEL,
47
+ monitoring: {
48
+ connectionString: rawEnv.APPLICATIONINSIGHTS_CONNECTION_STRING,
49
+ samplingRatio: (rawEnv.APPINSIGHTS_SAMPLING_PERCENTAGE ?? 5) / 100,
50
+ },
51
+ port,
52
+ };
53
+ }
54
+ /**
55
+ * Parses a comma-separated list of organization names from a string.
56
+ * Defaults to ["pagopa"] if the list is empty or undefined.
57
+ *
58
+ * @param value - Comma-separated organization names
59
+ * @returns Array of organization names
60
+ */
61
+ export function parseRequiredOrganizations(value) {
62
+ const organizations = (value ?? "")
63
+ .split(",")
64
+ .map((org) => org.trim())
65
+ .filter(Boolean);
66
+ return organizations.length > 0 ? organizations : ["pagopa"];
67
+ }
@@ -1,29 +1,26 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { emitCustomEvent } from "@pagopa/azure-tracing/logger";
3
+ import { filterUndefined } from "../utils/filter-undefined.js";
3
4
  const logger = getLogger(["mcpserver", "prompt-logging"]);
4
5
  /**
5
6
  * Decorator that adds logging to prompt load functions.
6
7
  * Logs when a prompt is requested to both console and Azure Application Insights.
7
8
  */
8
- export function withPromptLogging(prompt, catalogId) {
9
+ export function withPromptLogging(prompt, catalogId, requestId) {
9
10
  const originalLoad = prompt.load;
10
11
  return {
11
12
  ...prompt,
12
13
  load: async (args) => {
13
- const eventData = {
14
- arguments: JSON.stringify(args),
15
- promptId: catalogId,
16
- promptName: prompt.name,
17
- timestamp: new Date().toISOString(),
18
- };
19
14
  // Log to console (goes to CloudWatch in Lambda)
20
- logger.debug(`Prompt requested: ${prompt.name} - ${JSON.stringify(eventData)}`);
15
+ logger.debug(`Prompt requested: ${prompt.name} (ID: ${catalogId})`);
21
16
  // Emit custom event to Azure Application Insights
22
- emitCustomEvent("PromptRequested", {
23
- arguments: JSON.stringify(args),
17
+ // Note: Arguments are not logged to avoid exposing sensitive data
18
+ emitCustomEvent("PromptRequested", filterUndefined({
24
19
  promptId: catalogId,
25
20
  promptName: prompt.name,
26
- })("mcpserver");
21
+ requestId,
22
+ timestamp: new Date().toISOString(),
23
+ }))("mcpserver");
27
24
  // Call the original load function and return the result
28
25
  return await originalLoad(args);
29
26
  },
@@ -1,5 +1,6 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { emitCustomEvent } from "@pagopa/azure-tracing/logger";
3
+ import { filterUndefined } from "../utils/filter-undefined.js";
3
4
  const logger = getLogger(["mcpserver", "tool-logging"]);
4
5
  /**
5
6
  * Decorator that adds logging to tool execute functions.
@@ -7,36 +8,34 @@ const logger = getLogger(["mcpserver", "tool-logging"]);
7
8
  * Preserves the exact original function signature and type.
8
9
  */
9
10
  export function withToolLogging(tool) {
10
- if (typeof tool !== "object" || !tool.execute || !tool.name) {
11
- return tool;
12
- }
13
11
  const originalExecute = tool.execute;
14
12
  const toolName = tool.name;
15
13
  return {
16
14
  ...tool,
17
- execute: async (...args) => {
15
+ execute: async (args, context) => {
16
+ // Cast args to the expected type for tool execution
18
17
  const startTime = Date.now();
19
- const [toolArgs] = args;
20
- const eventData = {
21
- arguments: JSON.stringify(toolArgs),
22
- timestamp: new Date().toISOString(),
23
- toolName,
24
- };
25
18
  // Log to console (goes to CloudWatch in Lambda)
26
- logger.debug(`Tool executed: ${toolName} - ${JSON.stringify(eventData)}`);
19
+ logger.debug(`Tool executed: ${toolName}`);
27
20
  // Emit custom event to Azure Application Insights
21
+ const eventData = filterUndefined({
22
+ requestId: context?.requestId,
23
+ timestamp: new Date().toISOString(),
24
+ toolName,
25
+ });
28
26
  emitCustomEvent("ToolExecuted", eventData)("mcpserver");
29
27
  try {
30
28
  // Call the original execute function and return the result
31
- const result = await originalExecute(...args);
29
+ const result = await originalExecute(args, context);
32
30
  const executionTime = Date.now() - startTime;
33
31
  // Log successful completion
34
32
  logger.debug(`Tool completed: ${toolName} - execution time: ${executionTime}ms`);
35
33
  // Emit completion event to Azure Application Insights
36
- emitCustomEvent("ToolCompleted", {
34
+ emitCustomEvent("ToolCompleted", filterUndefined({
37
35
  executionTimeMs: executionTime.toString(),
36
+ requestId: context?.requestId,
38
37
  toolName,
39
- })("mcpserver");
38
+ }))("mcpserver");
40
39
  return result;
41
40
  }
42
41
  catch (error) {
@@ -44,11 +43,12 @@ export function withToolLogging(tool) {
44
43
  // Log error
45
44
  logger.error(`Tool failed: ${toolName} - ${error instanceof Error ? error.message : String(error)} - execution time: ${executionTime}ms`);
46
45
  // Emit error event to Azure Application Insights
47
- emitCustomEvent("ToolFailed", {
46
+ emitCustomEvent("ToolFailed", filterUndefined({
48
47
  error: error instanceof Error ? error.message : String(error),
49
48
  executionTimeMs: executionTime.toString(),
49
+ requestId: context?.requestId,
50
50
  toolName,
51
- })("mcpserver");
51
+ }))("mcpserver");
52
52
  throw error;
53
53
  }
54
54
  },
@@ -0,0 +1,54 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { z } from "zod";
3
+ import { retrieveAndGenerate } from "../services/bedrock-retrieve-and-generate.js";
4
+ import { resolveToWebsiteUrl } from "../services/bedrock.js";
5
+ import { parseJsonBody, sendErrorResponse, sendJsonResponse, } from "../utils/http.js";
6
+ const AskBodySchema = z.object({
7
+ query: z
8
+ .string({
9
+ invalid_type_error: "Missing required field: query",
10
+ required_error: "Missing required field: query",
11
+ })
12
+ .trim()
13
+ .min(1, "Missing required field: query"),
14
+ });
15
+ export async function handleAskEndpoint(req, res, config, kbRuntimeClient) {
16
+ const logger = getLogger(["mcpserver", "handler", "ask"]);
17
+ logger.debug("Handling /ask endpoint");
18
+ try {
19
+ let jsonBody;
20
+ try {
21
+ jsonBody = await parseJsonBody(req);
22
+ }
23
+ catch {
24
+ return sendErrorResponse(res, 400, "Invalid JSON in request body");
25
+ }
26
+ const result = AskBodySchema.safeParse(jsonBody);
27
+ if (!result.success) {
28
+ return sendErrorResponse(res, 400, result.error.errors[0].message);
29
+ }
30
+ const { query } = result.data;
31
+ const response = await retrieveAndGenerate(config.aws.knowledgeBaseId, config.aws.modelArn, query, kbRuntimeClient);
32
+ // Extract unique source URLs from citations
33
+ const sourceUrls = new Set();
34
+ response.citations?.forEach((citation) => {
35
+ citation.retrievedReferences?.forEach((ref) => {
36
+ const webLocation = resolveToWebsiteUrl(ref.location);
37
+ if (webLocation?.webLocation?.url) {
38
+ sourceUrls.add(webLocation.webLocation.url);
39
+ }
40
+ });
41
+ });
42
+ sendJsonResponse(res, 200, {
43
+ answer: response.output?.text || "",
44
+ sources: Array.from(sourceUrls),
45
+ });
46
+ }
47
+ catch (error) {
48
+ const errorMessage = error instanceof Error ? error.message : String(error);
49
+ logger.error(`Error handling /ask request: ${errorMessage}`);
50
+ sendErrorResponse(res, 500, "Internal server error", {
51
+ message: errorMessage,
52
+ });
53
+ }
54
+ }
@@ -0,0 +1,60 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { z } from "zod";
3
+ import { queryKnowledgeBaseStructured } from "../services/bedrock.js";
4
+ import { parseJsonBody, sendErrorResponse, sendJsonResponse, } from "../utils/http.js";
5
+ const SearchBodySchema = z.object({
6
+ number_of_results: z
7
+ .number({
8
+ invalid_type_error: "number_of_results must be between 1 and 20",
9
+ })
10
+ .int()
11
+ .min(1, "number_of_results must be between 1 and 20")
12
+ .max(20, "number_of_results must be between 1 and 20")
13
+ .optional()
14
+ .default(5),
15
+ query: z
16
+ .string({
17
+ invalid_type_error: "Missing required field: query",
18
+ required_error: "Missing required field: query",
19
+ })
20
+ .trim()
21
+ .min(1, "Missing required field: query"),
22
+ });
23
+ export async function handleSearchEndpoint(req, res, config, kbRuntimeClient) {
24
+ const logger = getLogger(["mcpserver", "handler", "search"]);
25
+ logger.debug("Handling /search endpoint");
26
+ try {
27
+ let jsonBody;
28
+ try {
29
+ jsonBody = await parseJsonBody(req);
30
+ }
31
+ catch {
32
+ return sendErrorResponse(res, 400, "Invalid JSON in request body");
33
+ }
34
+ const result = SearchBodySchema.safeParse(jsonBody);
35
+ if (!result.success) {
36
+ return sendErrorResponse(res, 400, result.error.errors[0].message);
37
+ }
38
+ const { number_of_results: numberOfResults, query } = result.data;
39
+ const results = await queryKnowledgeBaseStructured(config.aws.knowledgeBaseId, query, kbRuntimeClient, numberOfResults, config.aws.rerankingEnabled);
40
+ // Format results with content, score, and source URL
41
+ const formattedResults = results.map((result) => ({
42
+ content: result.content,
43
+ score: result.score,
44
+ source: result.location?.type === "WEB"
45
+ ? result.location.webLocation?.url
46
+ : undefined,
47
+ }));
48
+ sendJsonResponse(res, 200, {
49
+ query,
50
+ results: formattedResults,
51
+ });
52
+ }
53
+ catch (error) {
54
+ const errorMessage = error instanceof Error ? error.message : String(error);
55
+ logger.error(`Error handling /search request: ${errorMessage}`);
56
+ sendErrorResponse(res, 500, "Internal server error", {
57
+ message: errorMessage,
58
+ });
59
+ }
60
+ }
package/dist/index.js CHANGED
@@ -1,62 +1,20 @@
1
- import { getLogger } from "@logtape/logtape";
1
+ /**
2
+ * PagoPA DX Knowledge Retrieval MCP Server
3
+ *
4
+ * This module exposes the main server startup logic without triggering side effects
5
+ * at import time. All runtime setup flows from the main entrypoint.
6
+ */
2
7
  import { getEnabledPrompts } from "@pagopa/dx-mcpprompts";
3
- import { FastMCP } from "fastmcp";
4
- import { verifyGithubUser } from "./auth/github.js";
8
+ import { loadConfig } from "./config.js";
5
9
  import { configureLogging } from "./config/logging.js";
6
10
  import { configureAzureMonitoring } from "./config/monitoring.js";
7
- import { serverInstructions } from "./config/server.js";
8
- import { withPromptLogging } from "./decorators/promptUsageMonitoring.js";
9
- import { withToolLogging } from "./decorators/toolUsageMonitoring.js";
10
- import { QueryPagoPADXDocumentationTool } from "./tools/QueryPagoPADXDocumentation.js";
11
- import { SearchGitHubCodeTool } from "./tools/SearchGitHubCode.js";
12
- // Configure logging
13
- await configureLogging();
14
- await configureAzureMonitoring();
15
- const logger = getLogger(["mcpserver"]);
16
- logger.info("MCP Server starting...");
17
- // Authentication is enabled based on the AUTH_REQUIRED environment variable.
18
- const server = new FastMCP({
19
- authenticate: async (request) => {
20
- const authHeader = request.headers["x-gh-pat"];
21
- const apiKey = typeof authHeader === "string"
22
- ? authHeader
23
- : Array.isArray(authHeader)
24
- ? authHeader[0]
25
- : undefined;
26
- if (!apiKey || !(await verifyGithubUser(apiKey))) {
27
- throw new Response(null, {
28
- status: 401,
29
- statusText: "Unauthorized",
30
- });
31
- }
32
- // The returned object is accessible in the `context.session`.
33
- return {
34
- id: 1,
35
- token: apiKey,
36
- };
37
- },
38
- instructions: serverInstructions,
39
- name: "PagoPA DX Knowledge Retrieval MCP Server",
40
- version: "0.0.0",
41
- });
42
- logger.debug(`Server instructions: \n\n${serverInstructions}`);
43
- logger.debug(`Loading enabled prompts...`);
44
- getEnabledPrompts().then((prompts) => {
45
- prompts.forEach((catalogEntry) => {
46
- logger.debug(`Adding prompt: ${catalogEntry.prompt.name}`);
47
- // Apply logging decorator to the prompt
48
- const decoratedPrompt = withPromptLogging(catalogEntry.prompt, catalogEntry.id);
49
- server.addPrompt(decoratedPrompt);
50
- logger.debug(`Added prompt: ${catalogEntry.prompt.name}`);
51
- });
52
- });
53
- server.addTool(withToolLogging(QueryPagoPADXDocumentationTool));
54
- server.addTool(withToolLogging(SearchGitHubCodeTool));
55
- // Starts the server in HTTP Stream mode.
56
- server.start({
57
- httpStream: {
58
- port: 8080,
59
- stateless: true,
60
- },
61
- transportType: "httpStream",
62
- });
11
+ import { startHttpServer } from "./server.js";
12
+ export async function main(env) {
13
+ const config = loadConfig(env);
14
+ await configureLogging(config.logLevel);
15
+ configureAzureMonitoring(config.monitoring);
16
+ const enabledPrompts = await getEnabledPrompts();
17
+ const httpServer = await startHttpServer(config, enabledPrompts);
18
+ return httpServer;
19
+ }
20
+ export { startHttpServer };
@@ -0,0 +1,88 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import packageJson from "../../package.json" with { type: "json" };
5
+ import { withPromptLogging } from "../decorators/prompt-usage-monitoring.js";
6
+ import { withToolLogging } from "../decorators/tool-usage-monitoring.js";
7
+ import { sessionStorage } from "../session.js";
8
+ export function createServer({ enabledPrompts, requestId, toolDefinitions, }) {
9
+ const logger = getLogger(["mcpserver"]);
10
+ const mcpServer = new McpServer({
11
+ name: "pagopa-dx-mcp-server",
12
+ version: packageJson.version,
13
+ });
14
+ /**
15
+ * Register tools dynamically from the tool registry.
16
+ * This pattern allows tools to be added/removed by simply updating the registry,
17
+ * without needing to modify this registration code.
18
+ */
19
+ toolDefinitions.forEach(({ id, requiresSession, tool: toolDef }) => {
20
+ const decoratedTool = withToolLogging(toolDef);
21
+ const { annotations } = decoratedTool;
22
+ // Ensure parameters is a ZodObject (all tools must use z.object())
23
+ if (!(decoratedTool.parameters instanceof z.ZodObject)) {
24
+ throw new Error(`Tool "${id}" must use z.object() for parameters schema`);
25
+ }
26
+ const zodObject = decoratedTool.parameters;
27
+ mcpServer.registerTool(id, {
28
+ annotations: {
29
+ destructiveHint: annotations.destructiveHint ?? false,
30
+ idempotentHint: annotations.idempotentHint ?? true,
31
+ openWorldHint: annotations.openWorldHint ?? true,
32
+ readOnlyHint: annotations.readOnlyHint ?? true,
33
+ },
34
+ description: decoratedTool.description,
35
+ inputSchema: zodObject.shape,
36
+ title: annotations.title,
37
+ }, async (args) => {
38
+ const store = sessionStorage.getStore();
39
+ const context = requiresSession
40
+ ? {
41
+ requestId: store?.requestId,
42
+ session: store,
43
+ }
44
+ : undefined;
45
+ const result = await decoratedTool.execute(args, context);
46
+ return {
47
+ content: [
48
+ {
49
+ text: typeof result === "string" ? result : JSON.stringify(result),
50
+ type: "text",
51
+ },
52
+ ],
53
+ };
54
+ });
55
+ });
56
+ // Register prompts using the modern registerPrompt pattern
57
+ enabledPrompts.forEach((catalogEntry) => {
58
+ const decoratedPrompt = withPromptLogging(catalogEntry.prompt, catalogEntry.id, requestId);
59
+ // Build Zod schema from prompt arguments
60
+ const argsSchemaShape = {};
61
+ for (const arg of catalogEntry.prompt.arguments) {
62
+ const fieldSchema = z.string().describe(arg.description);
63
+ argsSchemaShape[arg.name] = arg.required
64
+ ? fieldSchema
65
+ : fieldSchema.optional();
66
+ }
67
+ const zodObject = z.object(argsSchemaShape);
68
+ mcpServer.registerPrompt(catalogEntry.prompt.name, {
69
+ argsSchema: zodObject.shape,
70
+ description: catalogEntry.prompt.description,
71
+ }, async (args) => {
72
+ const content = await decoratedPrompt.load(args || {});
73
+ return {
74
+ messages: [
75
+ {
76
+ content: {
77
+ text: content,
78
+ type: "text",
79
+ },
80
+ role: "user",
81
+ },
82
+ ],
83
+ };
84
+ });
85
+ });
86
+ logger.debug(`Server initialized with ${toolDefinitions.length} tools and ${enabledPrompts.length} prompts`);
87
+ return mcpServer;
88
+ }