Skip to main content
Glama
index.js23.3 kB
import { z } from "zod"; import axios from "axios"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Helper to create OpenProject API client from config or env, never throws function getOpenProjectApi(config) { const OPENPROJECT_API_KEY = config?.OPENPROJECT_API_KEY || process.env.OPENPROJECT_API_KEY; const OPENPROJECT_URL = config?.OPENPROJECT_URL || process.env.OPENPROJECT_URL; const OPENPROJECT_API_VERSION = config?.OPENPROJECT_API_VERSION || process.env.OPENPROJECT_API_VERSION || "v3"; if (!OPENPROJECT_API_KEY || !OPENPROJECT_URL) { // Do not throw, just return null return null; } return axios.create({ baseURL: `${OPENPROJECT_URL}/api/${OPENPROJECT_API_VERSION}`, headers: { 'Authorization': `Basic ${Buffer.from(`apikey:${OPENPROJECT_API_KEY}`).toString('base64')}`, 'Content-Type': 'application/json' } }); } export const setupMCPServer = () => { const server = new McpServer({ name: "stateless-server", version: "1.0.0", }, { capabilities: { logging: {} } }); // Register a prompt template that allows the server to // provide the context structure and (optionally) the variables // that should be placed inside of the prompt for client to fill in. server.prompt("greeting-template", "A simple greeting prompt template", { name: z.string().describe("Name to include in greeting"), }, async ({ name }) => { return { messages: [ { role: "user", content: { type: "text", text: `Please greet ${name} in a friendly manner.`, }, }, ], }; }); // Register a tool specifically for testing the ability // to resume notification streams to the client server.tool("start-notification-stream", "Starts sending periodic notifications for testing resumability", { interval: z .number() .describe("Interval in milliseconds between notifications") .default(100), count: z .number() .describe("Number of notifications to send (0 for 100)") .default(10), }, async ({ interval, count }, { sendNotification }) => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let counter = 0; while (count === 0 || counter < count) { counter++; try { await sendNotification({ method: "notifications/message", params: { level: "info", data: `Periodic notification #${counter} at ${new Date().toISOString()}`, }, }); } catch (error) { console.error("Error sending notification:", error); } await sleep(interval); } return { content: [ { type: "text", text: `Started sending periodic notifications every ${interval}ms`, }, ], }; }); // Create a resource that can be fetched by the client through // this MCP server. server.resource("greeting-resource", "https://example.com/greetings/default", { mimeType: "text/plain" }, async () => { return { contents: [ { uri: "https://example.com/greetings/default", text: "Hello, world!", }, ], }; }); // --- OpenProject Tools --- // All OpenProject tools now lazy-load the API client and config server.tool("openproject-create-project", "Creates a new project in OpenProject", { name: z.string().describe("Name of the project"), identifier: z.string().describe("Identifier of the project (unique)"), description: z.string().optional().describe("Optional description for the project"), }, async (params, context) => { const { name, identifier, description } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { const response = await openProjectApi.post('/projects', { name, identifier, description: description || "", }); return { content: [ { type: "text", text: `Successfully created project: ${response.data.name} (ID: ${response.data.id})`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error creating project: ${errorMsg}`, }, ], }; } }); server.tool("openproject-create-task", "Creates a new task (work package) in an OpenProject project", { projectId: z.string().describe("The ID or identifier of the project to add the task to"), subject: z.string().describe("Subject/title of the task"), description: z.string().optional().describe("Optional description for the task"), type: z.string().default("/api/v3/types/1").describe("Type of the work package (e.g., /api/v3/types/1 for Task)"), }, async (params, context) => { const { projectId, subject, description, type } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { const response = await openProjectApi.post(`/projects/${projectId}/work_packages`, { subject, description: { raw: description || "" }, _links: { type: { href: type }, project: { href: `/api/v3/projects/${projectId}` } } }); return { content: [ { type: "text", text: `Successfully created task: ${response.data.subject} (ID: ${response.data.id}) in project ${projectId}`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error creating task: ${errorMsg}`, }, ], }; } }); server.tool("openproject-get-project", "Gets a specific project by its ID from OpenProject", { projectId: z.string().describe("The ID of the project to retrieve"), }, async (params, context) => { const { projectId } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { const response = await openProjectApi.get(`/projects/${projectId}`); return { content: [ { type: "text", text: `Successfully retrieved project: ${response.data.name}`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error getting project: ${errorMsg}`, }, ], }; } }); server.tool("openproject-list-projects", "Lists all projects in OpenProject", { pageSize: z.number().optional().describe("Number of projects per page"), offset: z.number().optional().describe("Page number to retrieve (1-indexed)") }, async (params, context) => { const { pageSize, offset } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { const params = {}; if (pageSize) params.pageSize = pageSize; if (offset) params.offset = offset; const response = await openProjectApi.get('/projects', { params }); return { content: [ { type: "text", text: `Successfully retrieved ${response.data._embedded.elements.length} projects (Total: ${response.data.total})`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error listing projects: ${errorMsg}`, }, ], }; } }); server.tool("openproject-get-task", "Gets a specific task (work package) by its ID from OpenProject", { taskId: z.string().describe("The ID of the task to retrieve"), }, async (params, context) => { const { taskId } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { const response = await openProjectApi.get(`/work_packages/${taskId}`); return { content: [ { type: "text", text: `Successfully retrieved task: ${response.data.subject}`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error getting task: ${errorMsg}`, }, ], }; } }); server.tool("openproject-list-tasks", "Lists tasks (work packages) in OpenProject, optionally filtered by project ID", { projectId: z.string().optional().describe("Optional ID of the project to filter tasks by"), pageSize: z.number().optional().describe("Number of tasks per page"), offset: z.number().optional().describe("Page number to retrieve (1-indexed)") }, async (params, context) => { const { projectId, pageSize, offset } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { let url = '/work_packages'; const params = {}; if (pageSize) params.pageSize = pageSize; if (offset) params.offset = offset; if (projectId) { url = `/projects/${projectId}/work_packages`; } const response = await openProjectApi.get(url, { params }); return { content: [ { type: "text", text: `Successfully retrieved ${response.data._embedded.elements.length} tasks (Total: ${response.data.total})`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error listing tasks: ${errorMsg}`, }, ], }; } }); server.tool("openproject-update-project", "Updates an existing project in OpenProject. Only include fields to be changed.", { projectId: z.string().describe("The ID of the project to update"), name: z.string().optional().describe("New name for the project"), description: z.string().optional().describe("New description for the project"), }, async (params, context) => { const { projectId, name, description } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } const updatePayload = {}; if (name) updatePayload.name = name; if (description) updatePayload.description = description; if (Object.keys(updatePayload).length === 0) { return { content: [ { type: "text", text: "Error: No fields provided to update for the project." } ] }; } try { const response = await openProjectApi.patch(`/projects/${projectId}`, updatePayload); return { content: [ { type: "text", text: `Successfully updated project: ${response.data.name}`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error updating project: ${errorMsg}`, }, ], }; } }); server.tool("openproject-update-task", "Updates an existing task (work package) in OpenProject. Only include fields to be changed.", { taskId: z.string().describe("The ID of the task to update"), lockVersion: z.number().describe("The lockVersion of the task (obtained from a GET request)"), subject: z.string().optional().describe("New subject/title for the task"), description: z.string().optional().describe("New description for the task (provide as raw text)"), }, async (params, context) => { const { taskId, lockVersion, subject, description } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } const updatePayload = { lockVersion }; if (subject) updatePayload.subject = subject; if (description) updatePayload.description = { raw: description }; if (Object.keys(updatePayload).filter(k => k !== 'lockVersion').length === 0) { return { content: [ { type: "text", text: "Error: No fields (besides lockVersion) provided to update for the task." } ] }; } try { const response = await openProjectApi.patch(`/work_packages/${taskId}`, updatePayload); return { content: [ { type: "text", text: `Successfully updated task: ${response.data.subject}`, }, { type: "text", text: JSON.stringify(response.data), } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; return { content: [ { type: "text", text: `Error updating task: ${errorMsg}`, }, ], }; } }); server.tool("openproject-delete-project", "Deletes a project from OpenProject. This action is irreversible.", { projectId: z.string().describe("The ID of the project to delete"), }, async (params, context) => { const { projectId } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { await openProjectApi.delete(`/projects/${projectId}`); return { content: [ { type: "text", text: `Successfully deleted project with ID: ${projectId}`, } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; if (error.response?.status === 404) { return { content: [ { type: "text", text: `Project with ID ${projectId} not found. It might have already been deleted.` } ] }; } return { content: [ { type: "text", text: `Error deleting project: ${errorMsg}`, }, ], }; } }); server.tool("openproject-delete-task", "Deletes a task (work package) from OpenProject. This action is irreversible.", { taskId: z.string().describe("The ID of the task to delete"), }, async (params, context) => { const { taskId } = params; const openProjectApi = getOpenProjectApi(context?.config); if (!openProjectApi) { return { content: [ { type: "text", text: "OpenProject configuration is missing. Please provide OPENPROJECT_API_KEY and OPENPROJECT_URL." } ] }; } try { await openProjectApi.delete(`/work_packages/${taskId}`); return { content: [ { type: "text", text: `Successfully deleted task with ID: ${taskId}`, } ], }; } catch (error) { const errorMsg = error.message || (error.response?.data && JSON.stringify(error.response.data)) || "Unknown error"; if (error.response?.status === 404) { return { content: [ { type: "text", text: `Task with ID ${taskId} not found. It might have already been deleted.` } ] }; } return { content: [ { type: "text", text: `Error deleting task: ${errorMsg}`, }, ], }; } }); return server; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jessebautista/mcp-openproject-smithery'

If you have feedback or need assistance with the MCP directory API, please join our Discord server