Creating first version of MCP server.
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled

This commit is contained in:
2025-07-22 07:11:59 +00:00
parent f10bf53522
commit bf088da9d5
38 changed files with 6398 additions and 9442 deletions

View File

@ -1,91 +1,499 @@
'use strict';
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import 'reflect-metadata';
import ws from 'ws'; // yarn add ws
import express from 'express'; //express
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { useServer } from 'graphql-ws/use/ws';
import { execute, subscribe } from 'graphql';
import GraphQLserver from '@GraphQL/server';// Server of GraphQL,
import expressPlayground from 'graphql-playground-middleware-express';
import schema from '@GraphQL/schema';
import { config } from '@config';
import apiRouter from '@routes';
// Import tools and utilities
import { createDiagram, validateCreateDiagramInput, getSupportedDiagramTypes, getDiagramTypeDescription } from './tools/create-diagram.js';
import { createFileConfig, findDiagramFiles, getDiagramFilesWithMetadata } from './utils/file-manager.js';
import { createVSCodeConfig, openDiagramInVSCode, setupVSCodeEnvironment } from './utils/vscode-integration.js';
import { DiagramType, DiagramFormat } from './types/diagram-types.js';
const app = express(), //creating app
whitelist = config.WHITELIST_URLS,
corsOptions = {
origin: function (origin: string | undefined, callback: (arg0: Error | null, arg1?: boolean) => void) {
if (whitelist.indexOf(origin as string) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
// Functional types and configurations
type ServerConfig = Readonly<{
name: string;
version: string;
workspaceRoot: string;
capabilities: Readonly<{
resources: Record<string, unknown>;
tools: Record<string, unknown>;
}>;
}>;
//Inicialization of services of express
app
.use(cookieParser())
.use(express.urlencoded({limit: '500mb', extended: true}))
.use(express.json({limit: '500mb'}))
.use(cors(corsOptions))
.use(apiRouter)//Routes de App
.use('/graphql', GraphQLserver);//Server of Graphql
type ToolHandler = (args: any) => Promise<any>;
type ResourceHandler = (uri: string) => Promise<any>;
if(config.PLAYGROUND_GRAPHQL === true){
app.get('/playground', expressPlayground({
endpoint: '/graphql',
subscriptionEndpoint: '/graphql',
settings: {
'request.credentials': 'include', //Include Credentials for playground
},
}));
}
type ToolHandlers = Readonly<{
create_diagram: ToolHandler;
list_diagrams: ToolHandler;
open_diagram_in_vscode: ToolHandler;
setup_vscode_environment: ToolHandler;
get_diagram_types: ToolHandler;
}>;
// DO NOT DO app.listen() unless we're testing this directly
if (require.main === module) {
type ResourceHandlers = Readonly<{
'diagrams://workspace/list': ResourceHandler;
'diagrams://types/supported': ResourceHandler;
}>;
const server = app.listen(config.PORT, () => {
// create and use the websocket server
const wsServer = new ws.Server({
server,
path: '/graphql',
});
// Pure function to create server configuration
const createServerConfig = (workspaceRoot?: string): ServerConfig => ({
name: 'drawio-mcp-server',
version: '0.1.0',
workspaceRoot: workspaceRoot || process.env.WORKSPACE_ROOT || process.cwd(),
capabilities: {
resources: {},
tools: {},
},
});
useServer({
schema,
execute,
subscribe,
// eslint-disable-next-line
onConnect: (ctx) => {
//console.log('Connect');
},
// eslint-disable-next-line
onSubscribe: (ctx, msg) => {
//console.log('Subscribe');
},
// eslint-disable-next-line
onNext: (ctx, msg, args, result) => {
//console.debug('Next');
},
// eslint-disable-next-line
onError: (ctx, msg, errors) => {
//console.error('Error');
},
// eslint-disable-next-line
onComplete: (ctx, msg) => {
//console.log('Complete');
},
}, wsServer);
// Pure function to create tool definitions
const createToolDefinitions = () => [
{
name: 'create_diagram',
description: 'Create a new diagram of specified type (BPMN, UML, ER, Network, Architecture, etc.)',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the diagram file (without extension)'
},
type: {
type: 'string',
enum: Object.values(DiagramType),
description: 'Type of diagram to create'
},
format: {
type: 'string',
enum: Object.values(DiagramFormat),
default: 'drawio',
description: 'File format for the diagram'
},
description: {
type: 'string',
description: 'Description of the diagram'
},
outputPath: {
type: 'string',
description: 'Output directory path (relative to workspace)'
},
// BPMN specific parameters
processName: {
type: 'string',
description: 'Name of the BPMN process'
},
tasks: {
type: 'array',
items: { type: 'string' },
description: 'List of tasks for BPMN process'
},
gatewayType: {
type: 'string',
enum: ['exclusive', 'parallel'],
description: 'Type of gateway for BPMN process'
},
branches: {
type: 'array',
items: {
type: 'array',
items: { type: 'string' }
},
description: 'Branches for BPMN gateway'
},
beforeGateway: {
type: 'array',
items: { type: 'string' },
description: 'Tasks before gateway'
},
afterGateway: {
type: 'array',
items: { type: 'string' },
description: 'Tasks after gateway'
},
// UML specific parameters
classes: {
type: 'array',
items: { type: 'string' },
description: 'List of classes for UML class diagram'
},
// ER specific parameters
entities: {
type: 'array',
items: { type: 'string' },
description: 'List of entities for ER diagram'
},
// Network/Architecture specific parameters
components: {
type: 'array',
items: { type: 'string' },
description: 'List of components for network or architecture diagram'
},
// Flowchart specific parameters
processes: {
type: 'array',
items: { type: 'string' },
description: 'List of processes for flowchart'
}
},
required: ['name', 'type']
}
},
{
name: 'list_diagrams',
description: 'List all diagram files in the workspace',
inputSchema: {
type: 'object',
properties: {
workspaceRoot: {
type: 'string',
description: 'Workspace root directory (optional)'
}
}
}
},
{
name: 'open_diagram_in_vscode',
description: 'Open a diagram file in VSCode with draw.io extension',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the diagram file'
},
workspaceRoot: {
type: 'string',
description: 'Workspace root directory (optional)'
}
},
required: ['filePath']
}
},
{
name: 'setup_vscode_environment',
description: 'Setup VSCode environment for draw.io (install extension if needed)',
inputSchema: {
type: 'object',
properties: {
workspaceRoot: {
type: 'string',
description: 'Workspace root directory (optional)'
}
}
}
},
{
name: 'get_diagram_types',
description: 'Get list of supported diagram types with descriptions',
inputSchema: {
type: 'object',
properties: {}
}
}
];
console.log(`Starting Express on port ${config.PORT} and iniciating server of web sockets`);
// Pure function to create resource definitions
const createResourceDefinitions = () => [
{
uri: 'diagrams://workspace/list',
name: 'Workspace Diagrams',
mimeType: 'application/json',
description: 'List of all diagram files in the workspace'
},
{
uri: 'diagrams://types/supported',
name: 'Supported Diagram Types',
mimeType: 'application/json',
description: 'List of supported diagram types and their descriptions'
}
];
});
// Pure function to create tool handlers
const createToolHandlers = (config: ServerConfig): ToolHandlers => ({
create_diagram: async (args: any) => {
if (!validateCreateDiagramInput(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid create_diagram arguments'
);
}
}
const result = await createDiagram({
...args,
workspaceRoot: args.workspaceRoot || config.workspaceRoot
});
// Instead do export the app:
export default app;
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
},
list_diagrams: async (args: any) => {
const workspaceRoot = args?.workspaceRoot || config.workspaceRoot;
const fileConfig = createFileConfig(workspaceRoot);
const diagrams = await getDiagramFilesWithMetadata(fileConfig)();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
count: diagrams.length,
diagrams: diagrams.map(d => ({
path: d.relativePath,
format: d.format,
size: d.stats.size,
modified: d.stats.mtime
}))
}, null, 2)
}
]
};
},
open_diagram_in_vscode: async (args: any) => {
if (!args?.filePath) {
throw new McpError(
ErrorCode.InvalidParams,
'filePath is required'
);
}
const workspaceRoot = args.workspaceRoot || config.workspaceRoot;
const vscodeConfig = createVSCodeConfig(workspaceRoot);
try {
await openDiagramInVSCode(vscodeConfig)(args.filePath);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Successfully opened diagram: ${args.filePath}`
}, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Failed to open diagram: ${error}`
}, null, 2)
}
]
};
}
},
setup_vscode_environment: async (args: any) => {
const workspaceRoot = args?.workspaceRoot || config.workspaceRoot;
const vscodeConfig = createVSCodeConfig(workspaceRoot);
const result = await setupVSCodeEnvironment(vscodeConfig)();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
},
get_diagram_types: async () => {
const types = getSupportedDiagramTypes();
const typesWithDescriptions = types.map(type => ({
type,
description: getDiagramTypeDescription(type)
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
supportedTypes: typesWithDescriptions
}, null, 2)
}
]
};
}
});
// Pure function to create resource handlers
const createResourceHandlers = (config: ServerConfig): ResourceHandlers => ({
'diagrams://workspace/list': async () => {
const fileConfig = createFileConfig(config.workspaceRoot);
const diagrams = await getDiagramFilesWithMetadata(fileConfig)();
return {
contents: [
{
uri: 'diagrams://workspace/list',
mimeType: 'application/json',
text: JSON.stringify(diagrams, null, 2)
}
]
};
},
'diagrams://types/supported': async () => {
const types = getSupportedDiagramTypes();
const typesWithDescriptions = types.map(type => ({
type,
description: getDiagramTypeDescription(type)
}));
return {
contents: [
{
uri: 'diagrams://types/supported',
mimeType: 'application/json',
text: JSON.stringify(typesWithDescriptions, null, 2)
}
]
};
}
});
// Pure function to setup tool request handlers
const setupToolRequestHandlers = (server: Server, toolHandlers: ToolHandlers) => {
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: createToolDefinitions()
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const handler = toolHandlers[request.params.name as keyof ToolHandlers];
if (!handler) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
return await handler(request.params.arguments);
} catch (error) {
console.error(`Error in tool ${request.params.name}:`, error);
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
return server;
};
// Pure function to setup resource request handlers
const setupResourceRequestHandlers = (server: Server, resourceHandlers: ResourceHandlers) => {
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: createResourceDefinitions()
}));
// Handle resource requests
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
try {
const handler = resourceHandlers[uri as keyof ResourceHandlers];
if (!handler) {
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown resource URI: ${uri}`
);
}
return await handler(uri);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to read resource: ${error}`
);
}
});
return server;
};
// Pure function to setup error handling
const setupErrorHandling = (server: Server) => {
server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await server.close();
process.exit(0);
});
return server;
};
// Pure function to create and configure server
const createMCPServer = (config: ServerConfig): Server => {
const server = new Server(
{
name: config.name,
version: config.version,
},
{
capabilities: config.capabilities,
}
);
const toolHandlers = createToolHandlers(config);
const resourceHandlers = createResourceHandlers(config);
// Compose server setup using function composition
const serverWithTools = setupToolRequestHandlers(server, toolHandlers);
const serverWithResources = setupResourceRequestHandlers(serverWithTools, resourceHandlers);
const serverWithErrorHandling = setupErrorHandling(serverWithResources);
return serverWithErrorHandling;
};
// Pure function to run the server
const runServer = async (server: Server): Promise<void> => {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Draw.io MCP server running on stdio');
};
// Main execution - functional composition
const main = async (): Promise<void> => {
const config = createServerConfig();
const server = createMCPServer(config);
await runServer(server);
};
// Start the server
main().catch(console.error);