UNPKG

tray-mcp-server

Version:

A Model Context Protocol (MCP) server that provides tools to interact with Tray.io's integration platform API, including comprehensive workflow migration capabilities

1,216 lines (1,211 loc) 58 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { readFile } from 'fs/promises'; import { join } from 'path'; import { homedir } from 'os'; import { existsSync } from 'fs'; import { setupTrayMCP } from './setup.js'; // Constants for Tray API const TRAY_API_BASE = "https://api.tray.io"; const TRAY_EU_API_BASE = "https://api.eu1.tray.io"; const TRAY_APAC_API_BASE = "https://api.ap1.tray.io"; const USER_AGENT = "tray-mcp-server/1.0.0"; // Function to load configuration from file async function loadConfiguration() { try { const homeDir = homedir(); const configPath = join(homeDir, '.config', 'tray-mcp-server', 'config.json'); if (existsSync(configPath)) { const content = await readFile(configPath, 'utf-8'); const config = JSON.parse(content); return config; } } catch (error) { // Config doesn't exist or is invalid, fall back to environment variables } // Fall back to environment variables (supporting both old and new token names) const config = { masterToken: process.env.TRAY_MASTER_TOKEN || process.env.TRAY_TOKEN_US || process.env.TRAY_TOKEN_EU || process.env.TRAY_TOKEN_APAC, userToken: process.env.TRAY_USER_TOKEN, region: process.env.TRAY_REGION || 'us', workspaceId: process.env.TRAY_WORKSPACE_ID }; // If no master token found, try region-specific tokens if (!config.masterToken) { if (process.env.TRAY_TOKEN_US) { config.masterToken = process.env.TRAY_TOKEN_US; config.region = 'us'; } else if (process.env.TRAY_TOKEN_EU) { config.masterToken = process.env.TRAY_TOKEN_EU; config.region = 'eu'; } else if (process.env.TRAY_TOKEN_APAC) { config.masterToken = process.env.TRAY_TOKEN_APAC; config.region = 'apac'; } } return config; } // Check for command line arguments async function handleCommandLineArgs() { const args = process.argv.slice(2); if (args.includes('--setup')) { await setupTrayMCP(); return true; // Exit after setup } if (args.includes('--help') || args.includes('-h')) { console.log(` Tray MCP Server - Model Context Protocol server for Tray.io Usage: tray-mcp-server [options] Options: --setup Run interactive token setup --help, -h Show this help message Configuration: Tokens can be configured via: 1. Interactive setup: tray-mcp-server --setup 2. Environment variables: TRAY_TOKEN_US, TRAY_TOKEN_EU, TRAY_TOKEN_APAC 3. Configuration file: ~/.config/tray-mcp-server/config.json For Claude Desktop MCP configuration: { "tray-mcp-server": { "command": "npx", "args": ["tray-mcp-server"], "env": { "TRAY_TOKEN_US": "your-us-token-here" } } } For more information, visit: https://github.com/guilherme-x/tray-mcp-server `); return true; // Exit after help } return false; // Continue with normal execution } // Create server instance const server = new McpServer({ name: "tray-mcp-server", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }); // Helper function for making Tray API requests async function makeTrayRequest(url, token, options = {}) { const { method = "GET", body, region = "us" } = options; let baseUrl = TRAY_API_BASE; if (region === "eu") baseUrl = TRAY_EU_API_BASE; if (region === "apac") baseUrl = TRAY_APAC_API_BASE; const headers = { "Authorization": `Bearer ${token}`, "User-Agent": USER_AGENT, "Accept": "application/json", }; if (body && method !== "GET") { headers["Content-Type"] = "application/json"; } try { const response = await fetch(`${baseUrl}${url}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}, message: ${await response.text()}`); } return (await response.json()); } catch (error) { console.error("Error making Tray request:", error); return null; } } // Tool: List Connectors server.tool("list-connectors", "List all available connectors from Tray's connector library", { token: z.string().describe("Tray API token (master token or user token)"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/connectors?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: "Failed to retrieve connectors from Tray API", }, ], }; } const connectors = data.elements || []; if (connectors.length === 0) { return { content: [ { type: "text", text: "No connectors found", }, ], }; } const connectorsText = connectors .map((connector) => `**${connector.title}** (${connector.name} v${connector.version})\n` + `Description: ${connector.description}\n` + `Service ID: ${connector.service.id}\n` + `Service: ${connector.service.name} v${connector.service.version}\n`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${connectors.length} connectors:\n\n${connectorsText}${paginationInfo}`, }, ], }; }); // Tool: Get Connector Operations server.tool("get-connector-operations", "Get all available operations for a specific connector", { token: z.string().describe("Tray API token (master token or user token)"), connectorName: z.string().describe("Name of the connector (e.g., 'slack', 'salesforce')"), connectorVersion: z.string().describe("Version of the connector (e.g., '9.0')"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, connectorName, connectorVersion, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/connectors/${connectorName}/versions/${connectorVersion}/operations?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to retrieve operations for connector ${connectorName} v${connectorVersion}`, }, ], }; } const operations = data.elements || []; if (operations.length === 0) { return { content: [ { type: "text", text: `No operations found for connector ${connectorName} v${connectorVersion}`, }, ], }; } const operationsText = operations .map((op) => `**${op.title}** (${op.name})\n` + `Description: ${op.description}\n` + `Dynamic Output: ${op.hasDynamicOutput}\n` + `Auth Scopes: ${op.authScopes ? op.authScopes.join(", ") : "None"}\n` + `Input Schema: ${JSON.stringify(op.inputSchema, null, 2)}`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${operations.length} operations for ${connectorName} v${connectorVersion}:\n\n${operationsText}${paginationInfo}`, }, ], }; }); // Tool: Call Connector server.tool("call-connector", "Execute a connector operation with specified inputs", { token: z.string().describe("Tray API token (user token required for end-user operations)"), connectorName: z.string().describe("Name of the connector"), connectorVersion: z.string().describe("Version of the connector"), operation: z.string().describe("Name of the operation to execute"), authId: z.string().describe("Authentication ID for the connector"), input: z.record(z.any()).describe("Input parameters for the operation"), returnOutputSchema: z.boolean().optional().describe("Return output schema for dynamic operations"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, connectorName, connectorVersion, operation, authId, input, returnOutputSchema, region = "us" }) => { const payload = { operation, authId, input, ...(returnOutputSchema && { returnOutputSchema }), }; const data = await makeTrayRequest(`/core/v1/connectors/${connectorName}/versions/${connectorVersion}/call`, token, { method: "POST", body: payload, region }); if (!data) { return { content: [ { type: "text", text: `Failed to execute operation ${operation} on connector ${connectorName} v${connectorVersion}`, }, ], }; } return { content: [ { type: "text", text: `Operation ${operation} executed successfully:\n\n${JSON.stringify(data, null, 2)}`, }, ], }; }); // Tool: List Triggers server.tool("list-triggers", "List all available triggers from Tray's trigger library", { token: z.string().describe("Tray API token (master token or user token)"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/triggers?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: "Failed to retrieve triggers from Tray API", }, ], }; } const triggers = data.elements || []; if (triggers.length === 0) { return { content: [ { type: "text", text: "No triggers found", }, ], }; } const triggersText = triggers .map((trigger) => `**${trigger.title}** (${trigger.name} v${trigger.version})\n` + `Description: ${trigger.description}\n` + `Service ID: ${trigger.service.id}\n` + `Service: ${trigger.service.name} v${trigger.service.version}\n`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${triggers.length} triggers:\n\n${triggersText}${paginationInfo}`, }, ], }; }); // Tool: Get Service Environments server.tool("get-service-environments", "Get service environments for authentication setup", { token: z.string().describe("Tray API token"), serviceName: z.string().describe("Name of the service"), serviceVersion: z.string().describe("Version of the service"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, serviceName, serviceVersion, region = "us" }) => { const data = await makeTrayRequest(`/core/v1/services/${serviceName}/versions/${serviceVersion}/environments`, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to retrieve service environments for ${serviceName} v${serviceVersion}`, }, ], }; } return { content: [ { type: "text", text: `Service environments for ${serviceName} v${serviceVersion}:\n\n${JSON.stringify(data, null, 2)}`, }, ], }; }); // Tool: Create Authentication server.tool("create-authentication", "Create a new authentication for a service", { token: z.string().describe("Tray API token (user token for end-user auths)"), name: z.string().describe("Name for the authentication"), serviceEnvironmentId: z.string().describe("Service environment ID"), userData: z.record(z.any()).optional().describe("User data for the authentication"), credentials: z.record(z.any()).describe("Credentials for the authentication"), scopes: z.array(z.string()).optional().describe("Scopes for OAuth services"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, name, serviceEnvironmentId, userData, credentials, scopes, region = "us" }) => { const payload = { name, serviceEnvironmentId, ...(userData && { userData }), credentials, ...(scopes && { scopes }), }; const data = await makeTrayRequest("/core/v1/authentications", token, { method: "POST", body: payload, region }); if (!data) { return { content: [ { type: "text", text: "Failed to create authentication", }, ], }; } return { content: [ { type: "text", text: `Authentication created successfully:\n\n${JSON.stringify(data, null, 2)}`, }, ], }; }); // Tool: List Workspaces server.tool("list-workspaces", "List all workspaces the token has access to", { token: z.string().describe("Tray API token with workspace access"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/workspaces?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: "Failed to retrieve workspaces from Tray API", }, ], }; } const workspaces = data.elements || []; if (workspaces.length === 0) { return { content: [ { type: "text", text: "No workspaces found", }, ], }; } const workspacesText = workspaces .map((workspace) => `**${workspace.name}** (${workspace.id})\n` + `Type: ${workspace.type}\n` + `${workspace.description ? `Description: ${workspace.description}\n` : ""}` + `${workspace.monthlyTaskLimit ? `Monthly Task Limit: ${workspace.monthlyTaskLimit}\n` : ""}`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${workspaces.length} workspaces:\n\n${workspacesText}${paginationInfo}`, }, ], }; }); // Tool: Create Subscription (for triggers) server.tool("create-subscription", "Create a subscription for real-time trigger events", { token: z.string().describe("Tray API token (user token for end-user subscriptions)"), triggerName: z.string().describe("Name of the trigger"), triggerVersion: z.string().describe("Version of the trigger"), operation: z.string().describe("Trigger operation name"), authenticationId: z.string().describe("Authentication ID for the trigger"), endpoint: z.string().describe("Your webhook endpoint URL"), name: z.string().describe("Name for the subscription"), input: z.record(z.any()).describe("Input parameters for the trigger operation"), externalId: z.string().optional().describe("External ID for the subscription"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, triggerName, triggerVersion, operation, authenticationId, endpoint, name, input, externalId, region = "us" }) => { const payload = { trigger: { name: triggerName, version: triggerVersion, }, operation, authenticationId, endpoint, name, input, ...(externalId && { externalId }), }; const data = await makeTrayRequest("/core/v1/subscriptions", token, { method: "POST", body: payload, region }); if (!data) { return { content: [ { type: "text", text: "Failed to create subscription", }, ], }; } return { content: [ { type: "text", text: `Subscription created successfully:\n\n${JSON.stringify(data, null, 2)}`, }, ], }; }); // Tool: Get Subscriptions server.tool("get-subscriptions", "List all subscriptions", { token: z.string().describe("Tray API token"), limit: z.number().optional().describe("Limit the number of results (default: 10)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, limit = 10, cursor, region = "us" }) => { let url = `/core/v1/subscriptions?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: "Failed to retrieve subscriptions", }, ], }; } // Add pagination info if available let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Subscriptions:\n\n${JSON.stringify(data, null, 2)}${paginationInfo}`, }, ], }; }); // Tool: Update Subscription server.tool("update-subscription", "Update an existing subscription", { token: z.string().describe("Tray API token"), subscriptionId: z.string().describe("ID of the subscription to update"), name: z.string().optional().describe("New name for the subscription"), endpoint: z.string().optional().describe("New webhook endpoint URL"), input: z.record(z.any()).optional().describe("New input parameters for the trigger operation"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, subscriptionId, name, endpoint, input, region = "us" }) => { const payload = {}; if (name) payload.name = name; if (endpoint) payload.endpoint = endpoint; if (input) payload.input = input; if (Object.keys(payload).length === 0) { return { content: [ { type: "text", text: "No update parameters provided. Please specify at least one of: name, endpoint, or input.", }, ], }; } const data = await makeTrayRequest(`/core/v1/subscriptions/${subscriptionId}`, token, { method: "PATCH", body: payload, region }); if (!data) { return { content: [ { type: "text", text: `Failed to update subscription ${subscriptionId}`, }, ], }; } return { content: [ { type: "text", text: `Subscription ${subscriptionId} updated successfully:\n\n${JSON.stringify(data, null, 2)}`, }, ], }; }); // Tool: Delete Subscription server.tool("delete-subscription", "Delete a subscription by ID", { token: z.string().describe("Tray API token"), subscriptionId: z.string().describe("ID of the subscription to delete"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, subscriptionId, region = "us" }) => { const response = await makeTrayRequest(`/core/v1/subscriptions/${subscriptionId}`, token, { method: "DELETE", region }); return { content: [ { type: "text", text: response === null ? "Failed to delete subscription" : `Subscription ${subscriptionId} deleted successfully`, }, ], }; }); // Tool: Delete Authentication server.tool("delete-authentication", "Delete an authentication by ID", { token: z.string().describe("Tray API token"), authenticationId: z.string().describe("Authentication ID to delete"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, authenticationId, region = "us" }) => { const response = await makeTrayRequest(`/core/v1/authentications/${authenticationId}`, token, { method: "DELETE", region }); return { content: [ { type: "text", text: response === null ? "Failed to delete authentication" : `Authentication ${authenticationId} deleted successfully`, }, ], }; }); // Tool: List Authentications server.tool("list-authentications", "List all authentications the token has access to", { token: z.string().describe("Tray API token"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/authentications?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: "Failed to retrieve authentications from Tray API", }, ], }; } const authentications = data.elements || []; if (authentications.length === 0) { return { content: [ { type: "text", text: "No authentications found", }, ], }; } const authsText = authentications .map((auth) => `**${auth.name}** (${auth.id})\n` + `Service Environment ID: ${auth.serviceEnvironmentId}\n` + `Scopes: ${auth.scopes ? auth.scopes.join(", ") : "None"}\n`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${authentications.length} authentications:\n\n${authsText}${paginationInfo}`, }, ], }; }); // Tool: Get Trigger Operations server.tool("get-trigger-operations", "Get all available operations for a specific trigger", { token: z.string().describe("Tray API token"), triggerName: z.string().describe("Name of the trigger"), triggerVersion: z.string().describe("Version of the trigger"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, triggerName, triggerVersion, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/triggers/${triggerName}/versions/${triggerVersion}/operations?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to retrieve operations for trigger ${triggerName} v${triggerVersion}`, }, ], }; } const operations = data.elements || []; if (operations.length === 0) { return { content: [ { type: "text", text: `No operations found for trigger ${triggerName} v${triggerVersion}`, }, ], }; } const operationsText = operations .map((op) => `**${op.title}** (${op.name})\n` + `Description: ${op.description}\n` + `Dynamic Output: ${op.hasDynamicOutput}\n` + `Auth Scopes: ${op.authScopes ? op.authScopes.join(", ") : "None"}\n` + `Input Schema: ${JSON.stringify(op.inputSchema, null, 2)}`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${operations.length} operations for ${triggerName} v${triggerVersion}:\n\n${operationsText}${paginationInfo}`, }, ], }; }); // ================== WORKFLOW MIGRATION TOOLS ================== // Tool: List Projects server.tool("list-projects", "List all projects in a workspace for workflow migration analysis", { token: z.string().describe("Tray API token with project access"), workspaceId: z.string().describe("Workspace ID to list projects from"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, workspaceId, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/projects?workspaceId=${workspaceId}&limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to retrieve projects for workspace ${workspaceId}`, }, ], }; } const projects = data.elements || []; if (projects.length === 0) { return { content: [ { type: "text", text: `No projects found in workspace ${workspaceId}`, }, ], }; } const projectsText = projects .map((project) => `**${project.name}** (${project.id})\n` + `Description: ${project.description || "N/A"}\n` + `Created: ${new Date(project.createdAt).toLocaleDateString()}\n` + `Updated: ${new Date(project.updatedAt).toLocaleDateString()}\n`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${projects.length} projects in workspace ${workspaceId}:\n\n${projectsText}${paginationInfo}`, }, ], }; }); // Tool: List Project Versions server.tool("list-project-versions", "List all versions of a project for workflow migration analysis", { token: z.string().describe("Tray API token with project access"), projectId: z.string().describe("Project ID to list versions for"), limit: z.number().optional().describe("Limit the number of results (default: 50)"), cursor: z.string().optional().describe("Cursor for pagination"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, projectId, limit = 50, cursor, region = "us" }) => { let url = `/core/v1/projects/${projectId}/versions?limit=${limit}`; if (cursor) { url += `&cursor=${cursor}`; } const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to retrieve versions for project ${projectId}`, }, ], }; } const versions = data.elements || []; if (versions.length === 0) { return { content: [ { type: "text", text: `No versions found for project ${projectId}`, }, ], }; } const versionsText = versions .map((version) => `**Version ${version.versionNumber}** (${version.id})\n` + `Name: ${version.name}\n` + `Description: ${version.description || "N/A"}\n` + `Status: ${version.status}\n` + `Created: ${new Date(version.createdAt).toLocaleDateString()}\n`) .join("\n---\n"); // Add pagination info let paginationInfo = ""; if (data.cursor) { paginationInfo += `\n\n**Pagination:**\n`; paginationInfo += `Next cursor: ${data.cursor}\n`; if (data.hasMore !== undefined) { paginationInfo += `Has more: ${data.hasMore}\n`; } if (data.totalCount !== undefined) { paginationInfo += `Total count: ${data.totalCount}\n`; } } return { content: [ { type: "text", text: `Found ${versions.length} versions for project ${projectId}:\n\n${versionsText}${paginationInfo}`, }, ], }; }); // Tool: Export Project Version server.tool("export-project-version", "Export a complete project version with all workflow details and dependencies for migration", { token: z.string().describe("Tray API token with project access"), projectId: z.string().describe("Project ID to export"), versionNumber: z.string().describe("Version number to export"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, projectId, versionNumber, region = "us" }) => { const url = `/core/v1/projects/${projectId}/versions/${versionNumber}/export`; const data = await makeTrayRequest(url, token, { region }); if (!data) { return { content: [ { type: "text", text: `Failed to export project ${projectId} version ${versionNumber}`, }, ], }; } // Analyze the exported project structure const workflows = data.workflows || []; const connectors = data.connectors || []; const services = data.services || []; const authentications = data.authentications || []; const dependencies = data.dependencies || {}; // Create comprehensive migration analysis let analysisText = `# Project Export Analysis\n\n`; analysisText += `**Project:** ${data.project?.name || "Unknown"} (${projectId})\n`; analysisText += `**Version:** ${versionNumber}\n`; analysisText += `**Export Date:** ${new Date().toISOString()}\n\n`; // Workflow Analysis analysisText += `## Workflows (${workflows.length})\n\n`; workflows.forEach((workflow) => { analysisText += `### ${workflow.name} (${workflow.id})\n`; analysisText += `- **Description:** ${workflow.description || "N/A"}\n`; analysisText += `- **Enabled:** ${workflow.enabled}\n`; analysisText += `- **Steps:** ${workflow.steps?.length || 0}\n`; analysisText += `- **Connections:** ${workflow.connections?.length || 0}\n`; if (workflow.nestedWorkflows && workflow.nestedWorkflows.length > 0) { analysisText += `- **Nested Workflows:** ${workflow.nestedWorkflows.length}\n`; workflow.nestedWorkflows.forEach((nested) => { analysisText += ` - ${nested.workflowName} (${nested.workflowId})\n`; }); } if (workflow.triggers && workflow.triggers.length > 0) { analysisText += `- **Triggers:**\n`; workflow.triggers.forEach((trigger) => { analysisText += ` - ${trigger.name}: ${trigger.connector?.name || trigger.trigger?.name}`; if (trigger.connector?.operation || trigger.trigger?.operation) { analysisText += ` (${trigger.connector?.operation || trigger.trigger?.operation})`; } analysisText += `\n`; }); } if (workflow.steps && workflow.steps.length > 0) { analysisText += `- **Step Details:**\n`; workflow.steps.forEach((step) => { analysisText += ` - **${step.name}** (${step.id})\n`; if (step.connector) { analysisText += ` - Connector: ${step.connector.name} v${step.connector.version}\n`; analysisText += ` - Operation: ${step.connector.operation}\n`; } if (step.trigger) { analysisText += ` - Trigger: ${step.trigger.name} v${step.trigger.version}\n`; analysisText += ` - Operation: ${step.trigger.operation}\n`; } if (step.authentication) { analysisText += ` - Authentication: ${step.authentication.name} (${step.authentication.id})\n`; } }); } analysisText += `\n`; }); // Dependencies Analysis analysisText += `## Dependencies\n\n`; analysisText += `- **Connectors:** ${connectors.length}\n`; analysisText += `- **Services:** ${services.length}\n`; analysisText += `- **Authentications:** ${authentications.length}\n`; if (dependencies.workflows && dependencies.workflows.length > 0) { analysisText += `- **Workflow Dependencies:** ${dependencies.workflows.join(", ")}\n`; } if (dependencies.connectors && dependencies.connectors.length > 0) { analysisText += `- **Connector Dependencies:** ${dependencies.connectors.join(", ")}\n`; } analysisText += `\n## Configuration\n\n`; if (data.config) { analysisText += `Configuration keys: ${Object.keys(data.config).length}\n`; analysisText += `\`\`\`json\n${JSON.stringify(data.config, null, 2)}\n\`\`\`\n\n`; } // Include the full export data for detailed analysis analysisText += `## Full Export Data\n\n`; analysisText += `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``; return { content: [ { type: "text", text: analysisText, }, ], }; }); // Tool: Get Project Import Requirements server.tool("get-project-import-requirements", "Analyze import requirements and dependencies for migrating a project to a new environment", { token: z.string().describe("Tray API token with project access"), projectId: z.string().describe("Destination project ID for import analysis"), exportedProjectJson: z.any().describe("Exported project JSON data"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, projectId, exportedProjectJson, region = "us" }) => { const url = `/core/v1/projects/${projectId}/imports/requirements`; const data = await makeTrayRequest(url, token, { method: "POST", body: { exportedProjectJson }, region }); if (!data) { return { content: [ { type: "text", text: `Failed to analyze import requirements for project ${projectId}`, }, ], }; } let requirementsText = `# Import Requirements Analysis\n\n`; requirementsText += `**Target Project:** ${projectId}\n`; requirementsText += `**Analysis Date:** ${new Date().toISOString()}\n\n`; // Authentication Requirements if (data.authenticationsRequirements && data.authenticationsRequirements.length > 0) { requirementsText += `## Authentication Requirements (${data.authenticationsRequirements.length})\n\n`; data.authenticationsRequirements.forEach((authReq) => { requirementsText += `### ${authReq.title}\n`; requirementsText += `- **Export ID:** ${authReq.authExportId}\n`; requirementsText += `- **Service:** ${authReq.service?.name} v${authReq.service?.version}\n`; requirementsText += `- **Scopes:** ${authReq.scopes?.join(", ") || "None"}\n`; if (authReq.resolvedAuthentication) { requirementsText += `- **Resolved Authentication:** ${authReq.resolvedAuthentication.title} (${authReq.resolvedAuthentication.id})\n`; } else { requirementsText += `- **Status:** ⚠️ Requires new authentication\n`; } requirementsText += `\n`; }); } // Configuration Requirements if (data.config) { requirementsText += `## Configuration Analysis\n\n`; requirementsText += `- **New Config Keys:** ${data.newConfigKeys ? "Yes" : "No"}\n`; requirementsText += `- **Unresolved Authentications:** ${data.unresolvedAuthentications ? "Yes" : "No"}\n\n`; if (data.config.source && data.config.target) { requirementsText += `### Source Configuration\n`; requirementsText += `\`\`\`json\n${JSON.stringify(data.config.source, null, 2)}\n\`\`\`\n\n`; requirementsText += `### Target Configuration\n`; requirementsText += `\`\`\`json\n${JSON.stringify(data.config.target, null, 2)}\n\`\`\`\n\n`; } } // Migration Action Items requirementsText += `## Migration Action Items\n\n`; if (data.unresolvedAuthentications) { requirementsText += `- ⚠️ **Create missing authentications** before import\n`; } if (data.newConfigKeys) { requirementsText += `- ⚠️ **Review and map new configuration keys**\n`; } if (!data.unresolvedAuthentications && !data.newConfigKeys) { requirementsText += `- ✅ **Ready for import** - No blocking issues found\n`; } requirementsText += `\n## Full Requirements Data\n\n`; requirementsText += `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``; return { content: [ { type: "text", text: requirementsText, }, ], }; }); // Tool: Preview Project Import server.tool("preview-project-import", "Preview the impact of importing a project with workflow migration analysis", { token: z.string().describe("Tray API token with project access"), projectId: z.string().describe("Destination project ID for import preview"), exportedProjectJson: z.any().describe("Exported project JSON data"), authenticationResolution: z.array(z.any()).optional().describe("Authentication mapping for import"), connectorMapping: z.array(z.any()).optional().describe("Connector mapping for import"), serviceMapping: z.array(z.any()).optional().describe("Service mapping for import"), configOverride: z.any().optional().describe("Configuration overrides"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, projectId, exportedProjectJson, authenticationResolution = [], connectorMapping = [], serviceMapping = [], configOverride, region = "us" }) => { const url = `/core/v1/projects/${projectId}/imports/previews`; const requestBody = { exportedProjectJson, authenticationResolution, connectorMapping, serviceMapping }; if (configOverride) { requestBody.configOverride = configOverride; } const data = await makeTrayRequest(url, token, { method: "POST", body: requestBody, region }); if (!data) { return { content: [ { type: "text", text: `Failed to preview import for project ${projectId}`, }, ], }; } let previewText = `# Import Preview Analysis\n\n`; previewText += `**Target Project:** ${data.importMetadata?.projectName || projectId}\n`; previewText += `**Source Project:** ${data.importMetadata?.importedFrom?.projectName || "Unknown"}\n`; if (data.importMetadata?.importedFrom?.projectVersion) { previewText += `**Source Version:** ${data.importMetadata.importedFrom.projectVersion}\n`; } previewText += `**Preview Date:** ${new Date().toISOString()}\n\n`; // Project Impact Analysis if (data.projectImpact) { previewText += `## Project Impact\n\n`; // Workflow Changes if (data.projectImpact.workflows) { const workflows = data.projectImpact.workflows; previewText += `### Workflow Changes\n`; previewText += `- **Created:** ${workflows.created?.length || 0}\n`; previewText += `- **Updated:** ${workflows.updated?.length || 0}\n`; previewText += `- **Removed:** ${workflows.removed?.length || 0}\n\n`; if (workflows.created && workflows.created.length > 0) { previewText += `#### Workflows to be Created:\n`; workflows.created.forEach((workflow) => { previewText += `- **${workflow.name}** (from ${workflow.importedFromId})\n`; }); previewText += `\n`; } if (workflows.updated && workflows.updated.length > 0) { previewText += `#### Workflows to be Updated:\n`; workflows.updated.forEach((workflow) => { previewText += `- **${workflow.name}** (${workflow.id}) ← from ${workflow.importedFromId}\n`; }); previewText += `\n`; } if (workflows.removed && workflows.removed.length > 0) { previewText += `#### Workflows to be Removed:\n`; workflows.removed.forEach((workflow) => { previewText += `- **${workflow.name}** (${workflow.id})\n`; }); previewText += `\n`; } } // Configuration Changes if (data.projectImpact.config) { const config = data.projectImpact.config; previewText += `### Configuration Changes\n`; previewText += `- **Created:** ${config.created?.length || 0}\n`; previewText += `- **Updated:** ${config.updated?.length || 0}\n`; previewText += `- **Removed:** ${config.removed?.length || 0}\n\n`; } } // Solution Impact (if applicable) if (data.solutionImpact) { previewText += `## Solution Impact\n\n`; previewText += `- **Change Type:** ${data.solutionImpact.changeType}\n`; previewText += `- **Breaking Changes:** ${data.solutionImpact.breakingChanges ? "⚠️ Yes" : "✅ No"}\n`; previewText += `- **Requires New User Input:** ${data.solutionImpact.requiresNewUserInput ? "Yes" : "No"}\n`; previewText += `- **Requires New System Input:** ${data.solutionImpact.requiresNewSystemInput ? "Yes" : "No"}\n\n`; if (data.solutionImpact.breakingChanges) { previewText += `⚠️ **Warning:** This import introduces breaking changes to the solution. Existing solution instances may require manual upgrade.\n\n`; } } // Migration Recommendations previewText += `## Migration Recommendations\n\n`; let hasWarnings = false; if (data.solutionImpact?.breakingChanges) { previewText += `- ⚠️ **Breaking Changes Detected:** Review solution instances before import\n`; hasWarnings = true; } if (data.projectImpact?.workflows?.removed?.length > 0) { previewText += `- ⚠️ **Workflows Will Be Removed:** ${data.projectImpact.workflows.removed.length} workflows\n`; hasWarnings = true; } if (data.solutionImpact?.requiresNewUserInput) { previewText += `- ⚠️ **User Input Required:** Solution will require new end-user configuration\n`; hasWarnings = true; } if (!hasWarnings) { previewText += `- ✅ **Safe to Import:** No breaking changes or warnings detected\n`; } previewText += `\n## Full Preview Data\n\n`; previewText += `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``; return { content: [ { type: "text", text: previewText, }, ], }; }); // Tool: Analyze Workflow Dependencies server.tool("analyze-workflow-dependencies", "Analyze workflow dependencies and nested workflow calls for migration planning", { token: z.string().describe("Tray API token with project access"), projectExport: z.any().describe("Exported project JSON containing workflow data"), region: z.enum(["us", "eu", "apac"]).optional().describe("Tray region (default: us)"), }, async ({ token, projectExport, region = "us" }) => { // Analyze the project export for workflow dependencies const workflows = projectExport.workflows || []; const authentications = projectExport.authentications || []; const connectors = projectExport.connectors || []; const services = projectExport.services || []; let analysisText = `# Workflow Dependencies Analysis\n\n`; analysisText += `**Analysis Date:** ${new Date().toISOString()}\n`; analysisText += `**Project:** ${projectExport.project?.name || "Unknown"}\n\n`; // Dependency Summary analysisText += `## Summary\n\n`; analysisText += `- **Total Workflows:** ${workflows.length}\n`; analysisText += `- **Total Connectors:** ${connectors.length}\n`; analysisText += `- **Total Services:** ${services.length}\n`; analysisText += `- **Total Authenti