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
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:
568
src/index.ts
568
src/index.ts
@ -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);
|
||||
|
Reference in New Issue
Block a user