leanix-pathfinder-mcp-server
Version:
MCP Server for LeanIX Pathfinder API - Enterprise Architecture Management Platform
325 lines • 11.4 kB
JavaScript
/**
* LeanIX Pathfinder MCP Server - Streamable Transport for Microsoft Copilot Studio
*
* This server provides the streamable HTTP transport required by Microsoft Copilot Studio.
* It wraps the existing MCP server functionality with an HTTP endpoint.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { LeanIXClient } from './client.js';
import express from 'express';
import cors from 'cors';
// Server configuration
const server = new Server({
name: 'leanix-pathfinder-streamable-mcp-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Initialize LeanIX client
let leanixClient = null;
function initializeClient() {
if (!leanixClient) {
const config = {
baseUrl: process.env.LEANIX_BASE_URL || '',
clientId: process.env.LEANIX_CLIENT_ID || '',
clientSecret: process.env.LEANIX_CLIENT_SECRET || '',
workspaceId: process.env.LEANIX_WORKSPACE_ID,
};
if (!config.baseUrl || !config.clientId || !config.clientSecret) {
throw new Error('Missing required LeanIX configuration. Please set LEANIX_BASE_URL, LEANIX_CLIENT_ID, and LEANIX_CLIENT_SECRET environment variables.');
}
leanixClient = new LeanIXClient(config);
}
return leanixClient;
}
// Tool definitions (same as before)
const tools = [
{
name: 'leanix_get_suggestions',
description: 'Get search suggestions for a given term',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description: 'Search term (2-1000 characters)',
},
count: {
type: 'number',
description: 'Number of suggestions (1-100)',
minimum: 1,
maximum: 100,
},
object: {
type: 'string',
description: 'Suggestions object type (default: factSheet)',
},
perType: {
type: 'boolean',
description: 'Group suggestions per object type',
},
useCaseId: {
type: 'string',
description: 'Optional use case ID',
},
},
required: ['q'],
},
},
{
name: 'leanix_list_fact_sheets',
description: 'List fact sheets with optional filtering',
inputSchema: {
type: 'object',
properties: {
ids: {
type: 'string',
description: 'Comma-separated list of fact sheet IDs',
},
types: {
type: 'string',
description: 'Comma-separated list of fact sheet types to filter by',
},
pageSize: {
type: 'number',
description: 'Number of results per page',
},
cursor: {
type: 'string',
description: 'Cursor for pagination',
},
relations: {
type: 'string',
description: 'Include relations in response',
},
},
},
},
{
name: 'leanix_get_fact_sheet',
description: 'Get a specific fact sheet by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The ID of the fact sheet to retrieve',
},
version: {
type: 'number',
description: 'Optional version number',
},
},
required: ['id'],
},
},
{
name: 'leanix_create_fact_sheet',
description: 'Create a new fact sheet',
inputSchema: {
type: 'object',
properties: {
factSheet: {
type: 'object',
description: 'The fact sheet data to create',
properties: {
name: { type: 'string' },
type: { type: 'string' },
displayName: { type: 'string' },
description: { type: 'string' },
},
required: ['name', 'type'],
},
},
required: ['factSheet'],
},
},
{
name: 'leanix_update_fact_sheet',
description: 'Update an existing fact sheet',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The ID of the fact sheet to update',
},
factSheet: {
type: 'object',
description: 'The fact sheet data to update',
},
relationTypes: {
type: 'string',
description: 'Comma-separated list of relation types to update',
},
},
required: ['id', 'factSheet'],
},
},
{
name: 'leanix_delete_fact_sheet',
description: 'Delete a fact sheet',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The ID of the fact sheet to delete',
},
comment: {
type: 'string',
description: 'Optional comment for the deletion',
},
},
required: ['id'],
},
},
{
name: 'leanix_execute_graphql',
description: 'Execute a GraphQL query or mutation',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The GraphQL query or mutation string',
},
variables: {
type: 'object',
description: 'Optional variables for the GraphQL query',
},
operationName: {
type: 'string',
description: 'Optional operation name for multi-operation queries',
},
},
required: ['query'],
},
},
];
// Register list tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Register call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const client = initializeClient();
let result;
// Same tool handling logic as the original server
switch (name) {
case 'leanix_get_suggestions': {
const { q, count, object, perType, useCaseId } = args;
const response = await client.get('/services/pathfinder/v1/suggestions', {
headers: {
'Accept': 'application/json',
},
});
result = {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
break;
}
case 'leanix_list_fact_sheets': {
const { ids, types, pageSize, cursor, relations } = args;
const queryParams = new URLSearchParams();
if (ids)
queryParams.append('ids', ids);
if (types)
queryParams.append('types', types);
if (pageSize)
queryParams.append('pageSize', pageSize.toString());
if (cursor)
queryParams.append('cursor', cursor);
if (relations)
queryParams.append('relations', relations);
const response = await client.get(`/services/pathfinder/v1/factSheets?${queryParams}`);
result = {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
break;
}
case 'leanix_get_fact_sheet': {
const { id, version } = args;
const queryParams = version ? `?version=${version}` : '';
const response = await client.get(`/services/pathfinder/v1/factSheets/${id}${queryParams}`);
result = {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
break;
}
default:
throw new Error(`Unknown tool: ${name}`);
}
return result;
}
catch (error) {
const errorMessage = error.response?.data?.errorMessage || error.message || 'Unknown error occurred';
const statusCode = error.response?.status || 500;
return {
content: [
{
type: 'text',
text: `Error ${statusCode}: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Create Express app for HTTP/SSE transport
const app = express();
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Start the streamable server
async function main() {
const port = process.env.PORT || 3000;
// Create HTTP server
const httpServer = app.listen(port, () => {
console.error(`LeanIX Pathfinder Streamable MCP Server started on port ${port}`);
});
// Setup SSE endpoint for MCP communication
app.get('/sse', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
// Create a simple SSE connection
const transport = new SSEServerTransport('/sse', res);
server.connect(transport).catch(console.error);
});
console.error('Streamable MCP Server ready for connections on /sse endpoint');
}
main().catch((error) => {
console.error('Server failed to start:', error);
process.exit(1);
});
//# sourceMappingURL=streamable-server.js.map