# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md └── server.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Redmine MCP Server for Cline This is a custom MCP (Model Context Protocol) server that integrates with Redmine, allowing you to interact with your Redmine projects and issues through the Cline VS Code extension. ## Prerequisites * **Node.js:** You need Node.js (version 18 or newer) installed on your system. * **Redmine Instance:** You need a running Redmine instance with the REST API enabled. * **Redmine API Key:** You need an API key for your Redmine user account. You can find this in your Redmine account settings (usually under "My Account" -> "API access key"). * **Cline:** You need the Cline VS Code extension installed and configured. ## Installation 1. **Clone the repository:** ```bash git clone https://github.com/ilask/Redmine-MCP.git cd Redmine-MCP ``` 2. **Install dependencies:** ```bash npm install ``` ## Configuration 1. **Set environment variables:** Create a `.env` file in the root of the project directory and add the following, replacing the placeholders with your actual Redmine hostname and API key: ``` REDMINE_HOST=your-redmine-host.com REDMINE_API_KEY=your-redmine-api-key ``` **Important:** Do not commit your `.env` file to version control! It contains sensitive information. The `.gitignore` file included in this repository should prevent it from being committed. ## Adding to Cline 1. **Open Cline Settings:** In VS Code, open the Cline extension and go to the MCP Server tab. 2. **Edit MCP Settings:** Click "Edit MCP Settings" to open the `cline_mcp_settings.json` file. 3. **Add the server:** Add the following entry to the `mcpServers` object, replacing the `args` path with the *absolute* path to the `server.js` file on your system: ```json { "mcpServers": { "redmine-server": { "command": "node", "args": ["C:\\Users\\yourusername\\path\\to\\Redmine-MCP\\server.js"], "disabled": false, "autoApprove": [] } } } ``` **Important:** Make sure to use double backslashes (`\\`) in the path on Windows. 4. **Save:** Save the `cline_mcp_settings.json` file. Cline should automatically detect the changes and start the server. ## Available Resources and Tools ### Resources * **`redmine://projects/{project_id}`:** This resource represents a Redmine project. Replace `{project_id}` with the actual ID of a project in your Redmine instance. You can use the `access_mcp_resource` tool in Cline to read the details of a project. For example: ``` <access_mcp_resource> <server_name>redmine-server</server_name> <uri>redmine://projects/123</uri> </access_mcp_resource> ``` (Replace `123` with a valid project ID). This will return the project details as JSON. ### Tools * **`create_issue`:** This tool allows you to create a new issue in Redmine. It takes the following parameters: * `project_id` (string, required): The ID of the project where the issue should be created. * `subject` (string, required): The subject of the issue. * `description` (string, required): The description of the issue. You can use the `use_mcp_tool` tool in Cline to call this tool. For example: ``` <use_mcp_tool> <server_name>redmine-server</server_name> <tool_name>create_issue</tool_name> <arguments> { "project_id": "456", "subject": "My New Issue", "description": "This is a test issue created via Cline." } </arguments> </use_mcp_tool> ``` (Replace `456` with a valid project ID). This will create a new issue in the specified project and return the issue details as JSON. ## Troubleshooting * **Connection closed error:** If you see an error like "MCP error -1: Connection closed", make sure that your `REDMINE_HOST` and `REDMINE_API_KEY` environment variables are correctly set. Also, ensure that your Redmine instance is accessible from your computer. * **Check server logs:** If you encounter issues, check the server's output in the VS Code terminal for any error messages. The server logs errors to the console. ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "redmine-mcp-server", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@modelcontextprotocol/sdk": "^1.5.0", "dotenv": "^16.4.7", "node-redmine": "^0.2.2", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- ```javascript const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } = require('@modelcontextprotocol/sdk/types.js'); const Redmine = require('node-redmine'); const z = require('zod'); require('dotenv').config(); const redmineHost = process.env.REDMINE_HOST; const redmineApiKey = process.env.REDMINE_API_KEY; if (!redmineHost || !redmineApiKey) { throw new Error('REDMINE_HOST and REDMINE_API_KEY environment variables must be set'); } const redmine = new Redmine(redmineHost, { apiKey: redmineApiKey }); const isValidCreateIssueArgs = ( args ) => typeof args === 'object' && args !== null && typeof args.project_id === 'string' && typeof args.subject === 'string' && typeof args.description === 'string'; class RedmineServer { server; constructor() { this.server = new Server( { name: 'redmine-mcp-server', version: '0.1.0', }, { capabilities: { resources: {}, tools: {}, }, } ); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupResourceHandlers() { this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ ], })); this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: 'redmine://projects/{project_id}', name: 'Redmine Project', mimeType: 'application/json', description: 'Details of a Redmine project', }, ], }) ); this.server.setRequestHandler( ReadResourceRequestSchema, async (request) => { const match = request.params.uri.match( /^redmine:\/\/projects\/([^/]+)$/ ); if (!match) { throw new McpError( ErrorCode.InvalidRequest, `Invalid URI format: ${request.params.uri}` ); } const projectId = match[1]; try { const project = await new Promise((resolve, reject) => { redmine.getProject(projectId, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(project), }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Redmine API error: ${error}` ); } } ); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create_issue', description: 'Create a new Redmine issue', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'Project ID', }, subject: { type: 'string', description: 'Issue subject', }, description: { type: 'string', description: 'Issue description', }, }, required: ['project_id', 'subject', 'description'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'create_issue') { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } if (!isValidCreateIssueArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid create_issue arguments' ); } const { project_id, subject, description } = request.params.arguments; try { const issue = await new Promise((resolve, reject) => { redmine.create_issue({ project_id: project_id, subject: subject, description: description, }, (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); return { content: [ { type: 'text', text: JSON.stringify(issue), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Redmine API error: ${error}`, }, ], isError: true, }; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Redmine MCP server running on stdio'); } } const server = new RedmineServer(); server.run().catch(console.error); ```