# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md └── server.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | .env 3 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Redmine MCP Server for Cline 2 | 3 | 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. 4 | 5 | ## Prerequisites 6 | 7 | * **Node.js:** You need Node.js (version 18 or newer) installed on your system. 8 | * **Redmine Instance:** You need a running Redmine instance with the REST API enabled. 9 | * **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"). 10 | * **Cline:** You need the Cline VS Code extension installed and configured. 11 | 12 | ## Installation 13 | 14 | 1. **Clone the repository:** 15 | ```bash 16 | git clone https://github.com/ilask/Redmine-MCP.git 17 | cd Redmine-MCP 18 | ``` 19 | 2. **Install dependencies:** 20 | ```bash 21 | npm install 22 | ``` 23 | 24 | ## Configuration 25 | 26 | 1. **Set environment variables:** 27 | 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: 28 | 29 | ``` 30 | REDMINE_HOST=your-redmine-host.com 31 | REDMINE_API_KEY=your-redmine-api-key 32 | ``` 33 | **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. 34 | 35 | ## Adding to Cline 36 | 37 | 1. **Open Cline Settings:** In VS Code, open the Cline extension and go to the MCP Server tab. 38 | 2. **Edit MCP Settings:** Click "Edit MCP Settings" to open the `cline_mcp_settings.json` file. 39 | 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: 40 | 41 | ```json 42 | { 43 | "mcpServers": { 44 | "redmine-server": { 45 | "command": "node", 46 | "args": ["C:\\Users\\yourusername\\path\\to\\Redmine-MCP\\server.js"], 47 | "disabled": false, 48 | "autoApprove": [] 49 | } 50 | } 51 | } 52 | ``` 53 | **Important:** Make sure to use double backslashes (`\\`) in the path on Windows. 54 | 4. **Save:** Save the `cline_mcp_settings.json` file. Cline should automatically detect the changes and start the server. 55 | 56 | ## Available Resources and Tools 57 | 58 | ### Resources 59 | 60 | * **`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: 61 | 62 | ``` 63 | <access_mcp_resource> 64 | <server_name>redmine-server</server_name> 65 | <uri>redmine://projects/123</uri> 66 | </access_mcp_resource> 67 | ``` 68 | (Replace `123` with a valid project ID). This will return the project details as JSON. 69 | 70 | ### Tools 71 | 72 | * **`create_issue`:** This tool allows you to create a new issue in Redmine. It takes the following parameters: 73 | * `project_id` (string, required): The ID of the project where the issue should be created. 74 | * `subject` (string, required): The subject of the issue. 75 | * `description` (string, required): The description of the issue. 76 | 77 | You can use the `use_mcp_tool` tool in Cline to call this tool. For example: 78 | 79 | ``` 80 | <use_mcp_tool> 81 | <server_name>redmine-server</server_name> 82 | <tool_name>create_issue</tool_name> 83 | <arguments> 84 | { 85 | "project_id": "456", 86 | "subject": "My New Issue", 87 | "description": "This is a test issue created via Cline." 88 | } 89 | </arguments> 90 | </use_mcp_tool> 91 | ``` 92 | (Replace `456` with a valid project ID). This will create a new issue in the specified project and return the issue details as JSON. 93 | 94 | ## Troubleshooting 95 | * **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. 96 | * **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. 97 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "redmine-mcp-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "dependencies": { 13 | "@modelcontextprotocol/sdk": "^1.5.0", 14 | "dotenv": "^16.4.7", 15 | "node-redmine": "^0.2.2", 16 | "zod": "^3.24.2" 17 | } 18 | } 19 | ``` -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- ```javascript 1 | const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); 2 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); 3 | const { 4 | CallToolRequestSchema, 5 | ErrorCode, 6 | ListResourcesRequestSchema, 7 | ListResourceTemplatesRequestSchema, 8 | ListToolsRequestSchema, 9 | McpError, 10 | ReadResourceRequestSchema, 11 | } = require('@modelcontextprotocol/sdk/types.js'); 12 | const Redmine = require('node-redmine'); 13 | const z = require('zod'); 14 | require('dotenv').config(); 15 | 16 | const redmineHost = process.env.REDMINE_HOST; 17 | const redmineApiKey = process.env.REDMINE_API_KEY; 18 | 19 | if (!redmineHost || !redmineApiKey) { 20 | throw new Error('REDMINE_HOST and REDMINE_API_KEY environment variables must be set'); 21 | } 22 | 23 | const redmine = new Redmine(redmineHost, { apiKey: redmineApiKey }); 24 | 25 | const isValidCreateIssueArgs = ( 26 | args 27 | ) => typeof args === 'object' && args !== null && typeof args.project_id === 'string' && typeof args.subject === 'string' && typeof args.description === 'string'; 28 | 29 | class RedmineServer { 30 | server; 31 | 32 | constructor() { 33 | this.server = new Server( 34 | { 35 | name: 'redmine-mcp-server', 36 | version: '0.1.0', 37 | }, 38 | { 39 | capabilities: { 40 | resources: {}, 41 | tools: {}, 42 | }, 43 | } 44 | ); 45 | 46 | this.setupResourceHandlers(); 47 | this.setupToolHandlers(); 48 | 49 | // Error handling 50 | this.server.onerror = (error) => console.error('[MCP Error]', error); 51 | process.on('SIGINT', async () => { 52 | await this.server.close(); 53 | process.exit(0); 54 | }); 55 | } 56 | 57 | setupResourceHandlers() { 58 | this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ 59 | resources: [ 60 | ], 61 | })); 62 | 63 | this.server.setRequestHandler( 64 | ListResourceTemplatesRequestSchema, 65 | async () => ({ 66 | resourceTemplates: [ 67 | { 68 | uriTemplate: 'redmine://projects/{project_id}', 69 | name: 'Redmine Project', 70 | mimeType: 'application/json', 71 | description: 'Details of a Redmine project', 72 | }, 73 | ], 74 | }) 75 | ); 76 | 77 | this.server.setRequestHandler( 78 | ReadResourceRequestSchema, 79 | async (request) => { 80 | const match = request.params.uri.match( 81 | /^redmine:\/\/projects\/([^/]+)$/ 82 | ); 83 | if (!match) { 84 | throw new McpError( 85 | ErrorCode.InvalidRequest, 86 | `Invalid URI format: ${request.params.uri}` 87 | ); 88 | } 89 | const projectId = match[1]; 90 | 91 | try { 92 | const project = await new Promise((resolve, reject) => { 93 | redmine.getProject(projectId, (err, data) => { 94 | if (err) { 95 | reject(err); 96 | } else { 97 | resolve(data); 98 | } 99 | }); 100 | }); 101 | 102 | return { 103 | contents: [ 104 | { 105 | uri: request.params.uri, 106 | mimeType: 'application/json', 107 | text: JSON.stringify(project), 108 | }, 109 | ], 110 | }; 111 | } catch (error) { 112 | throw new McpError( 113 | ErrorCode.InternalError, 114 | `Redmine API error: ${error}` 115 | ); 116 | } 117 | } 118 | ); 119 | } 120 | 121 | setupToolHandlers() { 122 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 123 | tools: [ 124 | { 125 | name: 'create_issue', 126 | description: 'Create a new Redmine issue', 127 | inputSchema: { 128 | type: 'object', 129 | properties: { 130 | project_id: { 131 | type: 'string', 132 | description: 'Project ID', 133 | }, 134 | subject: { 135 | type: 'string', 136 | description: 'Issue subject', 137 | }, 138 | description: { 139 | type: 'string', 140 | description: 'Issue description', 141 | }, 142 | }, 143 | required: ['project_id', 'subject', 'description'], 144 | }, 145 | }, 146 | ], 147 | })); 148 | 149 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 150 | if (request.params.name !== 'create_issue') { 151 | throw new McpError( 152 | ErrorCode.MethodNotFound, 153 | `Unknown tool: ${request.params.name}` 154 | ); 155 | } 156 | 157 | if (!isValidCreateIssueArgs(request.params.arguments)) { 158 | throw new McpError( 159 | ErrorCode.InvalidParams, 160 | 'Invalid create_issue arguments' 161 | ); 162 | } 163 | 164 | const { project_id, subject, description } = request.params.arguments; 165 | 166 | try { 167 | const issue = await new Promise((resolve, reject) => { 168 | redmine.create_issue({ 169 | project_id: project_id, 170 | subject: subject, 171 | description: description, 172 | }, (err, data) => { 173 | if (err) { 174 | reject(err); 175 | } else { 176 | resolve(data); 177 | } 178 | }); 179 | }); 180 | 181 | return { 182 | content: [ 183 | { 184 | type: 'text', 185 | text: JSON.stringify(issue), 186 | }, 187 | ], 188 | }; 189 | } catch (error) { 190 | return { 191 | content: [ 192 | { 193 | type: 'text', 194 | text: `Redmine API error: ${error}`, 195 | }, 196 | ], 197 | isError: true, 198 | }; 199 | } 200 | }); 201 | } 202 | 203 | async run() { 204 | const transport = new StdioServerTransport(); 205 | await this.server.connect(transport); 206 | console.error('Redmine MCP server running on stdio'); 207 | } 208 | } 209 | 210 | const server = new RedmineServer(); 211 | server.run().catch(console.error); 212 | ```