#
tokens: 2364/50000 4/4 files
lines: off (toggle) GitHub
raw markdown copy
# 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);

```