diff --git a/README.md b/README.md index 07a479b0..503a83f8 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,13 @@ Each MCP server is implemented with either the [Typescript MCP SDK](https://gith - **[Filesystem](src/filesystem)** - Secure file operations with configurable access controls - **[GitHub](src/github)** - Repository management, file operations, and GitHub API integration +- **[GitLab](src/gitlab)** - GitLab API, enabling project management +- **[Git](src/git)** - Tools to read, search, and manipulate Git repositories - **[Google Drive](src/gdrive)** - File access and search capabilities for Google Drive - **[PostgreSQL](src/postgres)** - Read-only database access with schema inspection +- **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities - **[Slack](src/slack)** - Channel management and messaging capabilities +- **[Sentry](src/sentry)** - Retrieving and analyzing issues from Sentry.io - **[Memory](src/memory)** - Knowledge graph-based persistent memory system - **[Puppeteer](src/puppeteer)** - Browser automation and web scraping - **[Brave Search](src/brave-search)** - Web and local search using Brave's Search API @@ -39,6 +43,8 @@ pip install mcp-server-git python -m mcp_server_git ``` +Follow [these](https://docs.astral.sh/uv/getting-started/installation/) instructions to install `uv` / `uvx` and [these](https://pip.pypa.io/en/stable/installation/) to install `pip`. + ### Using an MCP Client However, running a server on its own isn't very useful, and should instead be configured into an MCP client. For example, here's the Claude Desktop configuration to use the above server: @@ -99,7 +105,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 💬 Community -- [GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions) +- [GitHub Discussions](https://github.com/orgs/modelcontextprotocol/discussions) ## ⭐ Support diff --git a/package-lock.json b/package-lock.json index 8a0cd994..3079d7c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,6 +183,10 @@ "resolved": "src/github", "link": true }, + "node_modules/@modelcontextprotocol/server-gitlab": { + "resolved": "src/gitlab", + "link": true + }, "node_modules/@modelcontextprotocol/server-google-maps": { "resolved": "src/google-maps", "link": true @@ -3276,11 +3280,10 @@ }, "src/brave-search": { "name": "@modelcontextprotocol/server-brave-search", - "version": "0.5.1", + "version": "0.5.2", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "0.5.0", - "node-fetch": "^3.3.2" + "@modelcontextprotocol/sdk": "0.5.0" }, "bin": { "mcp-server-brave-search": "dist/index.js" @@ -3301,33 +3304,6 @@ "undici-types": "~6.19.2" } }, - "src/brave-search/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "src/brave-search/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "src/duckduckgo": { "name": "@modelcontextprotocol/server-duckduckgo", "version": "0.2.0", @@ -3519,6 +3495,58 @@ "url": "https://opencollective.com/node-fetch" } }, + "src/gitlab": { + "version": "0.5.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2", + "zod-to-json-schema": "^3.23.5" + }, + "bin": { + "mcp-server-gitlab": "dist/index.js" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/gitlab/node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "src/gitlab/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "src/gitlab/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "src/google-maps": { "name": "@modelcontextprotocol/server-google-maps", "version": "0.5.1", diff --git a/src/brave-search/README.md b/src/brave-search/README.md index 2d5caef9..2d5ae0df 100644 --- a/src/brave-search/README.md +++ b/src/brave-search/README.md @@ -38,11 +38,16 @@ Add this to your `claude_desktop_config.json`: ```json { - "brave-search": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-brave-search"], - "env": { - "BRAVE_API_KEY": "YOUR_API_KEY_HERE" + "mcpServers": { + "brave-search": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-brave-search" + ], + "env": { + "BRAVE_API_KEY": "YOUR_API_KEY_HERE" + } } } } diff --git a/src/brave-search/index.ts b/src/brave-search/index.ts index 596dc9f7..e0f61623 100644 --- a/src/brave-search/index.ts +++ b/src/brave-search/index.ts @@ -7,7 +7,6 @@ import { ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import fetch from "node-fetch"; const WEB_SEARCH_TOOL: Tool = { name: "brave_web_search", @@ -104,7 +103,7 @@ function checkRateLimit() { requestCount.lastReset = now; } if (requestCount.second >= RATE_LIMIT.perSecond || - requestCount.month >= RATE_LIMIT.perMonth) { + requestCount.month >= RATE_LIMIT.perMonth) { throw new Error('Rate limit exceeded'); } requestCount.second++; diff --git a/src/brave-search/package.json b/src/brave-search/package.json index 98413d4f..3e68d358 100644 --- a/src/brave-search/package.json +++ b/src/brave-search/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-brave-search", - "version": "0.5.1", + "version": "0.5.2", "description": "MCP server for Brave Search API integration", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -19,8 +19,7 @@ "watch": "tsc --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "0.5.0", - "node-fetch": "^3.3.2" + "@modelcontextprotocol/sdk": "0.5.0" }, "devDependencies": { "@types/node": "^20.10.0", diff --git a/src/everything/README.md b/src/everything/README.md index 69281a06..b00a6635 100644 --- a/src/everything/README.md +++ b/src/everything/README.md @@ -78,8 +78,14 @@ Add to your `claude_desktop_config.json`: ```json { - "everything": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-everything"] + "mcpServers": { + "everything": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ] + } } } +``` diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index 63cae6fa..04ecad3c 100644 --- a/src/fetch/src/mcp_server_fetch/server.py +++ b/src/fetch/src/mcp_server_fetch/server.py @@ -47,7 +47,7 @@ def get_robots_txt_url(url: str) -> str: async def check_may_autonomously_fetch_url(url: str, user_agent: str): """ Check if the URL can be fetched by the user agent according to the robots.txt file. - Raises an McpError if not. + Raises a McpError if not. """ from httpx import AsyncClient, HTTPError diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 08ec4617..c2950cd5 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -82,9 +82,16 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio Add this to your `claude_desktop_config.json`: ```json { - "filesystem": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop", "/path/to/other/allowed/dir"] + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/username/Desktop", + "/path/to/other/allowed/dir" + ] + } } } ``` diff --git a/src/gdrive/README.md b/src/gdrive/README.md index 16dadf2b..9a795f0c 100644 --- a/src/gdrive/README.md +++ b/src/gdrive/README.md @@ -51,9 +51,14 @@ To integrate this server with the desktop app, add the following to your app's s ```json { - "gdrive": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-gdrive"] + "mcpServers": { + "gdrive": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-gdrive" + ] + } } } ``` diff --git a/src/git/src/mcp_server_git/__init__.py b/src/git/src/mcp_server_git/__init__.py index 2dfb6e5c..22700187 100644 --- a/src/git/src/mcp_server_git/__init__.py +++ b/src/git/src/mcp_server_git/__init__.py @@ -1,5 +1,4 @@ import click -from functools import partial from pathlib import Path import logging import sys diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index d4fe3328..fe1e3f59 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -1,5 +1,4 @@ import logging -import json from pathlib import Path from typing import Sequence from mcp.server import Server @@ -14,8 +13,7 @@ ) from enum import Enum import git -from pydantic import BaseModel, Field -from typing import List, Optional +from pydantic import BaseModel class GitStatus(BaseModel): repo_path: str @@ -32,7 +30,7 @@ class GitCommit(BaseModel): class GitAdd(BaseModel): repo_path: str - files: List[str] + files: list[str] class GitReset(BaseModel): repo_path: str diff --git a/src/github/README.md b/src/github/README.md index d966721c..cfd268a8 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -117,11 +117,16 @@ To use this with Claude Desktop, add the following to your `claude_desktop_confi ```json { - "github": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-github"], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "" + "mcpServers": { + "github": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } } } } diff --git a/src/gitlab/README.md b/src/gitlab/README.md new file mode 100644 index 00000000..fdf82552 --- /dev/null +++ b/src/gitlab/README.md @@ -0,0 +1,132 @@ +# GitLab MCP Server + +MCP Server for the GitLab API, enabling project management, file operations, and more. + +### Features + +- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist +- **Comprehensive Error Handling**: Clear error messages for common issues +- **Git History Preservation**: Operations maintain proper Git history without force pushing +- **Batch Operations**: Support for both single-file and multi-file operations + + +## Tools + +1. `create_or_update_file` + - Create or update a single file in a project + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `file_path` (string): Path where to create/update the file + - `content` (string): Content of the file + - `commit_message` (string): Commit message + - `branch` (string): Branch to create/update the file in + - `previous_path` (optional string): Path of the file to move/rename + - Returns: File content and commit details + +2. `push_files` + - Push multiple files in a single commit + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `branch` (string): Branch to push to + - `files` (array): Files to push, each with `file_path` and `content` + - `commit_message` (string): Commit message + - Returns: Updated branch reference + +3. `search_repositories` + - Search for GitLab projects + - Inputs: + - `search` (string): Search query + - `page` (optional number): Page number for pagination + - `per_page` (optional number): Results per page (default 20) + - Returns: Project search results + +4. `create_repository` + - Create a new GitLab project + - Inputs: + - `name` (string): Project name + - `description` (optional string): Project description + - `visibility` (optional string): 'private', 'internal', or 'public' + - `initialize_with_readme` (optional boolean): Initialize with README + - Returns: Created project details + +5. `get_file_contents` + - Get contents of a file or directory + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `file_path` (string): Path to file/directory + - `ref` (optional string): Branch/tag/commit to get contents from + - Returns: File/directory contents + +6. `create_issue` + - Create a new issue + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `title` (string): Issue title + - `description` (optional string): Issue description + - `assignee_ids` (optional number[]): User IDs to assign + - `labels` (optional string[]): Labels to add + - `milestone_id` (optional number): Milestone ID + - Returns: Created issue details + +7. `create_merge_request` + - Create a new merge request + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `title` (string): MR title + - `description` (optional string): MR description + - `source_branch` (string): Branch containing changes + - `target_branch` (string): Branch to merge into + - `draft` (optional boolean): Create as draft MR + - `allow_collaboration` (optional boolean): Allow commits from upstream members + - Returns: Created merge request details + +8. `fork_repository` + - Fork a project + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `namespace` (optional string): Namespace to fork to + - Returns: Forked project details + +9. `create_branch` + - Create a new branch + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `branch` (string): Name for new branch + - `ref` (optional string): Source branch/commit for new branch + - Returns: Created branch reference + +## Setup + +### Personal Access Token +[Create a GitLab Personal Access Token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) with appropriate permissions: + - Go to User Settings > Access Tokens in GitLab + - Select the required scopes: + - `api` for full API access + - `read_api` for read-only access + - `read_repository` and `write_repository` for repository operations + - Create the token and save it securely + +### Usage with Claude Desktop +Add the following to your `claude_desktop_config.json`: + +```json +{ + "gitlab": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-gitlab"], + "env": { + "GITLAB_PERSONAL_ACCESS_TOKEN": "", + "GITLAB_API_URL": "https://gitlab.com/api/v4" // Optional, for self-hosted instances + } + } +} +``` + +## Environment Variables + +- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token (required) +- `GITLAB_API_URL`: Base URL for GitLab API (optional, defaults to `https://gitlab.com/api/v4`) + +## License + +This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. \ No newline at end of file diff --git a/src/gitlab/index.ts b/src/gitlab/index.ts new file mode 100644 index 00000000..e246af4d --- /dev/null +++ b/src/gitlab/index.ts @@ -0,0 +1,534 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import fetch from "node-fetch"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + GitLabForkSchema, + GitLabReferenceSchema, + GitLabRepositorySchema, + GitLabIssueSchema, + GitLabMergeRequestSchema, + GitLabContentSchema, + GitLabCreateUpdateFileResponseSchema, + GitLabSearchResponseSchema, + GitLabTreeSchema, + GitLabCommitSchema, + CreateRepositoryOptionsSchema, + CreateIssueOptionsSchema, + CreateMergeRequestOptionsSchema, + CreateBranchOptionsSchema, + CreateOrUpdateFileSchema, + SearchRepositoriesSchema, + CreateRepositorySchema, + GetFileContentsSchema, + PushFilesSchema, + CreateIssueSchema, + CreateMergeRequestSchema, + ForkRepositorySchema, + CreateBranchSchema, + type GitLabFork, + type GitLabReference, + type GitLabRepository, + type GitLabIssue, + type GitLabMergeRequest, + type GitLabContent, + type GitLabCreateUpdateFileResponse, + type GitLabSearchResponse, + type GitLabTree, + type GitLabCommit, + type FileOperation, +} from './schemas.js'; + +const server = new Server({ + name: "gitlab-mcp-server", + version: "0.5.1", +}, { + capabilities: { + tools: {} + } +}); + +const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; +const GITLAB_API_URL = process.env.GITLAB_API_URL || 'https://gitlab.com/api/v4'; + +if (!GITLAB_PERSONAL_ACCESS_TOKEN) { + console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set"); + process.exit(1); +} + +async function forkProject( + projectId: string, + namespace?: string +): Promise { + const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`; + const queryParams = namespace ? `?namespace=${encodeURIComponent(namespace)}` : ''; + + const response = await fetch(url + queryParams, { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabForkSchema.parse(await response.json()); +} + +async function createBranch( + projectId: string, + options: z.infer +): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`, + { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + branch: options.name, + ref: options.ref + }) + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabReferenceSchema.parse(await response.json()); +} + +async function getDefaultBranchRef(projectId: string): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`, + { + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` + } + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + const project = GitLabRepositorySchema.parse(await response.json()); + return project.default_branch; +} + +async function getFileContents( + projectId: string, + filePath: string, + ref?: string +): Promise { + const encodedPath = encodeURIComponent(filePath); + let url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`; + if (ref) { + url += `?ref=${encodeURIComponent(ref)}`; + } + + const response = await fetch(url, { + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` + } + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + const data = GitLabContentSchema.parse(await response.json()); + + if (!Array.isArray(data) && data.content) { + data.content = Buffer.from(data.content, 'base64').toString('utf8'); + } + + return data; +} + +async function createIssue( + projectId: string, + options: z.infer +): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`, + { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + title: options.title, + description: options.description, + assignee_ids: options.assignee_ids, + milestone_id: options.milestone_id, + labels: options.labels?.join(',') + }) + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabIssueSchema.parse(await response.json()); +} + +async function createMergeRequest( + projectId: string, + options: z.infer +): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`, + { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + title: options.title, + description: options.description, + source_branch: options.source_branch, + target_branch: options.target_branch, + allow_collaboration: options.allow_collaboration, + draft: options.draft + }) + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabMergeRequestSchema.parse(await response.json()); +} + +async function createOrUpdateFile( + projectId: string, + filePath: string, + content: string, + commitMessage: string, + branch: string, + previousPath?: string +): Promise { + const encodedPath = encodeURIComponent(filePath); + const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`; + + const body = { + branch, + content, + commit_message: commitMessage, + ...(previousPath ? { previous_path: previousPath } : {}) + }; + + // Check if file exists + let method = "POST"; + try { + await getFileContents(projectId, filePath, branch); + method = "PUT"; + } catch (error) { + // File doesn't exist, use POST + } + + const response = await fetch(url, { + method, + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabCreateUpdateFileResponseSchema.parse(await response.json()); +} + +async function createTree( + projectId: string, + files: FileOperation[], + ref?: string +): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`, + { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + files: files.map(file => ({ + file_path: file.path, + content: file.content + })), + ...(ref ? { ref } : {}) + }) + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabTreeSchema.parse(await response.json()); +} + +async function createCommit( + projectId: string, + message: string, + branch: string, + actions: FileOperation[] +): Promise { + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`, + { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + branch, + commit_message: message, + actions: actions.map(action => ({ + action: "create", + file_path: action.path, + content: action.content + })) + }) + } + ); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabCommitSchema.parse(await response.json()); +} + +async function searchProjects( + query: string, + page: number = 1, + perPage: number = 20 +): Promise { + const url = new URL(`${GITLAB_API_URL}/projects`); + url.searchParams.append("search", query); + url.searchParams.append("page", page.toString()); + url.searchParams.append("per_page", perPage.toString()); + + const response = await fetch(url.toString(), { + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` + } + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + const projects = await response.json(); + return GitLabSearchResponseSchema.parse({ + count: parseInt(response.headers.get("X-Total") || "0"), + items: projects + }); +} + +async function createRepository( + options: z.infer +): Promise { + const response = await fetch(`${GITLAB_API_URL}/projects`, { + method: "POST", + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + name: options.name, + description: options.description, + visibility: options.visibility, + initialize_with_readme: options.initialize_with_readme + }) + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText}`); + } + + return GitLabRepositorySchema.parse(await response.json()); +} + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "create_or_update_file", + description: "Create or update a single file in a GitLab project", + inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema) + }, + { + name: "search_repositories", + description: "Search for GitLab projects", + inputSchema: zodToJsonSchema(SearchRepositoriesSchema) + }, + { + name: "create_repository", + description: "Create a new GitLab project", + inputSchema: zodToJsonSchema(CreateRepositorySchema) + }, + { + name: "get_file_contents", + description: "Get the contents of a file or directory from a GitLab project", + inputSchema: zodToJsonSchema(GetFileContentsSchema) + }, + { + name: "push_files", + description: "Push multiple files to a GitLab project in a single commit", + inputSchema: zodToJsonSchema(PushFilesSchema) + }, + { + name: "create_issue", + description: "Create a new issue in a GitLab project", + inputSchema: zodToJsonSchema(CreateIssueSchema) + }, + { + name: "create_merge_request", + description: "Create a new merge request in a GitLab project", + inputSchema: zodToJsonSchema(CreateMergeRequestSchema) + }, + { + name: "fork_repository", + description: "Fork a GitLab project to your account or specified namespace", + inputSchema: zodToJsonSchema(ForkRepositorySchema) + }, + { + name: "create_branch", + description: "Create a new branch in a GitLab project", + inputSchema: zodToJsonSchema(CreateBranchSchema) + } + ] + }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + switch (request.params.name) { + case "fork_repository": { + const args = ForkRepositorySchema.parse(request.params.arguments); + const fork = await forkProject(args.project_id, args.namespace); + return { toolResult: fork }; + } + + case "create_branch": { + const args = CreateBranchSchema.parse(request.params.arguments); + let ref = args.ref; + if (!ref) { + ref = await getDefaultBranchRef(args.project_id); + } + + const branch = await createBranch(args.project_id, { + name: args.branch, + ref + }); + + return { toolResult: branch }; + } + + case "search_repositories": { + const args = SearchRepositoriesSchema.parse(request.params.arguments); + const results = await searchProjects(args.search, args.page, args.per_page); + return { toolResult: results }; + } + + case "create_repository": { + const args = CreateRepositorySchema.parse(request.params.arguments); + const repository = await createRepository(args); + return { toolResult: repository }; + } + + case "get_file_contents": { + const args = GetFileContentsSchema.parse(request.params.arguments); + const contents = await getFileContents(args.project_id, args.file_path, args.ref); + return { toolResult: contents }; + } + + case "create_or_update_file": { + const args = CreateOrUpdateFileSchema.parse(request.params.arguments); + const result = await createOrUpdateFile( + args.project_id, + args.file_path, + args.content, + args.commit_message, + args.branch, + args.previous_path + ); + return { toolResult: result }; + } + + case "push_files": { + const args = PushFilesSchema.parse(request.params.arguments); + const result = await createCommit( + args.project_id, + args.commit_message, + args.branch, + args.files.map(f => ({ path: f.file_path, content: f.content })) + ); + return { toolResult: result }; + } + + case "create_issue": { + const args = CreateIssueSchema.parse(request.params.arguments); + const { project_id, ...options } = args; + const issue = await createIssue(project_id, options); + return { toolResult: issue }; + } + + case "create_merge_request": { + const args = CreateMergeRequestSchema.parse(request.params.arguments); + const { project_id, ...options } = args; + const mergeRequest = await createMergeRequest(project_id, options); + return { toolResult: mergeRequest }; + } + + default: + throw new Error(`Unknown tool: ${request.params.name}`); + } + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + } + throw error; + } +}); + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("GitLab MCP Server running on stdio"); +} + +runServer().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/gitlab/package-lock.json b/src/gitlab/package-lock.json new file mode 100644 index 00000000..9cb28696 --- /dev/null +++ b/src/gitlab/package-lock.json @@ -0,0 +1,551 @@ +{ + "name": "@modelcontextprotocol/server-gitlab", + "version": "0.5.1", + "lockfileVersion": 1, + "requires": true, + "packages": { + "": { + "name": "@modelcontextprotocol/server-gitlab", + "version": "0.5.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2" + }, + "bin": { + "mcp-server-gitlab": "dist/index.js" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src/gitlab/package.json b/src/gitlab/package.json new file mode 100644 index 00000000..e4b35fff --- /dev/null +++ b/src/gitlab/package.json @@ -0,0 +1,31 @@ +{ + "name": "@modelcontextprotocol/server-gitlab", + "version": "0.5.1", + "description": "MCP server for using the GitLab API", + "license": "MIT", + "author": "GitLab, PBC (https://gitlab.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "type": "module", + "bin": { + "mcp-server-gitlab": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2", + "zod-to-json-schema": "^3.23.5" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} diff --git a/src/gitlab/schemas.ts b/src/gitlab/schemas.ts new file mode 100644 index 00000000..108c190b --- /dev/null +++ b/src/gitlab/schemas.ts @@ -0,0 +1,325 @@ +import { z } from 'zod'; + +// Base schemas for common types +export const GitLabAuthorSchema = z.object({ + name: z.string(), + email: z.string(), + date: z.string() +}); + +// Repository related schemas +export const GitLabOwnerSchema = z.object({ + username: z.string(), // Changed from login to match GitLab API + id: z.number(), + avatar_url: z.string(), + web_url: z.string(), // Changed from html_url to match GitLab API + name: z.string(), // Added as GitLab includes full name + state: z.string() // Added as GitLab includes user state +}); + +export const GitLabRepositorySchema = z.object({ + id: z.number(), + name: z.string(), + path_with_namespace: z.string(), // Changed from full_name to match GitLab API + visibility: z.string(), // Changed from private to match GitLab API + owner: GitLabOwnerSchema, + web_url: z.string(), // Changed from html_url to match GitLab API + description: z.string().nullable(), + fork: z.boolean(), + ssh_url_to_repo: z.string(), // Changed from ssh_url to match GitLab API + http_url_to_repo: z.string(), // Changed from clone_url to match GitLab API + created_at: z.string(), + last_activity_at: z.string(), // Changed from updated_at to match GitLab API + default_branch: z.string() +}); + +// File content schemas +export const GitLabFileContentSchema = z.object({ + file_name: z.string(), // Changed from name to match GitLab API + file_path: z.string(), // Changed from path to match GitLab API + size: z.number(), + encoding: z.string(), + content: z.string(), + content_sha256: z.string(), // Changed from sha to match GitLab API + ref: z.string(), // Added as GitLab requires branch reference + blob_id: z.string(), // Added to match GitLab API + last_commit_id: z.string() // Added to match GitLab API +}); + +export const GitLabDirectoryContentSchema = z.object({ + name: z.string(), + path: z.string(), + type: z.string(), + mode: z.string(), + id: z.string(), // Changed from sha to match GitLab API + web_url: z.string() // Changed from html_url to match GitLab API +}); + +export const GitLabContentSchema = z.union([ + GitLabFileContentSchema, + z.array(GitLabDirectoryContentSchema) +]); + +// Operation schemas +export const FileOperationSchema = z.object({ + path: z.string(), + content: z.string() +}); + +// Tree and commit schemas +export const GitLabTreeEntrySchema = z.object({ + id: z.string(), // Changed from sha to match GitLab API + name: z.string(), + type: z.enum(['blob', 'tree']), + path: z.string(), + mode: z.string() +}); + +export const GitLabTreeSchema = z.object({ + id: z.string(), // Changed from sha to match GitLab API + tree: z.array(GitLabTreeEntrySchema) +}); + +export const GitLabCommitSchema = z.object({ + id: z.string(), // Changed from sha to match GitLab API + short_id: z.string(), // Added to match GitLab API + title: z.string(), // Changed from message to match GitLab API + author_name: z.string(), + author_email: z.string(), + authored_date: z.string(), + committer_name: z.string(), + committer_email: z.string(), + committed_date: z.string(), + web_url: z.string(), // Changed from html_url to match GitLab API + parent_ids: z.array(z.string()) // Changed from parents to match GitLab API +}); + +// Reference schema +export const GitLabReferenceSchema = z.object({ + name: z.string(), // Changed from ref to match GitLab API + commit: z.object({ + id: z.string(), // Changed from sha to match GitLab API + web_url: z.string() // Changed from url to match GitLab API + }) +}); + +// Input schemas for operations +export const CreateRepositoryOptionsSchema = z.object({ + name: z.string(), + description: z.string().optional(), + visibility: z.enum(['private', 'internal', 'public']).optional(), // Changed from private to match GitLab API + initialize_with_readme: z.boolean().optional() // Changed from auto_init to match GitLab API +}); + +export const CreateIssueOptionsSchema = z.object({ + title: z.string(), + description: z.string().optional(), // Changed from body to match GitLab API + assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API + milestone_id: z.number().optional(), // Changed from milestone to match GitLab API + labels: z.array(z.string()).optional() +}); + +export const CreateMergeRequestOptionsSchema = z.object({ // Changed from CreatePullRequestOptionsSchema + title: z.string(), + description: z.string().optional(), // Changed from body to match GitLab API + source_branch: z.string(), // Changed from head to match GitLab API + target_branch: z.string(), // Changed from base to match GitLab API + allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API + draft: z.boolean().optional() +}); + +export const CreateBranchOptionsSchema = z.object({ + name: z.string(), // Changed from ref to match GitLab API + ref: z.string() // The source branch/commit for the new branch +}); + +// Response schemas for operations +export const GitLabCreateUpdateFileResponseSchema = z.object({ + file_path: z.string(), + branch: z.string(), + commit_id: z.string(), // Changed from sha to match GitLab API + content: GitLabFileContentSchema.optional() +}); + +export const GitLabSearchResponseSchema = z.object({ + count: z.number(), // Changed from total_count to match GitLab API + items: z.array(GitLabRepositorySchema) +}); + +// Fork related schemas +export const GitLabForkParentSchema = z.object({ + name: z.string(), + path_with_namespace: z.string(), // Changed from full_name to match GitLab API + owner: z.object({ + username: z.string(), // Changed from login to match GitLab API + id: z.number(), + avatar_url: z.string() + }), + web_url: z.string() // Changed from html_url to match GitLab API +}); + +export const GitLabForkSchema = GitLabRepositorySchema.extend({ + forked_from_project: GitLabForkParentSchema // Changed from parent to match GitLab API +}); + +// Issue related schemas +export const GitLabLabelSchema = z.object({ + id: z.number(), + name: z.string(), + color: z.string(), + description: z.string().optional() +}); + +export const GitLabUserSchema = z.object({ + username: z.string(), // Changed from login to match GitLab API + id: z.number(), + name: z.string(), + avatar_url: z.string(), + web_url: z.string() // Changed from html_url to match GitLab API +}); + +export const GitLabMilestoneSchema = z.object({ + id: z.number(), + iid: z.number(), // Added to match GitLab API + title: z.string(), + description: z.string(), + state: z.string(), + web_url: z.string() // Changed from html_url to match GitLab API +}); + +export const GitLabIssueSchema = z.object({ + id: z.number(), + iid: z.number(), // Added to match GitLab API + project_id: z.number(), // Added to match GitLab API + title: z.string(), + description: z.string(), // Changed from body to match GitLab API + state: z.string(), + author: GitLabUserSchema, + assignees: z.array(GitLabUserSchema), + labels: z.array(GitLabLabelSchema), + milestone: GitLabMilestoneSchema.nullable(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + web_url: z.string() // Changed from html_url to match GitLab API +}); + +// Merge Request related schemas (equivalent to Pull Request) +export const GitLabMergeRequestDiffRefSchema = z.object({ + base_sha: z.string(), + head_sha: z.string(), + start_sha: z.string() +}); + +export const GitLabMergeRequestSchema = z.object({ + id: z.number(), + iid: z.number(), // Added to match GitLab API + project_id: z.number(), // Added to match GitLab API + title: z.string(), + description: z.string(), // Changed from body to match GitLab API + state: z.string(), + merged: z.boolean(), + author: GitLabUserSchema, + assignees: z.array(GitLabUserSchema), + source_branch: z.string(), // Changed from head to match GitLab API + target_branch: z.string(), // Changed from base to match GitLab API + diff_refs: GitLabMergeRequestDiffRefSchema, + web_url: z.string(), // Changed from html_url to match GitLab API + created_at: z.string(), + updated_at: z.string(), + merged_at: z.string().nullable(), + closed_at: z.string().nullable(), + merge_commit_sha: z.string().nullable() +}); + +// API Operation Parameter Schemas +const ProjectParamsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path") // Changed from owner/repo to match GitLab API +}); + +export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ + file_path: z.string().describe("Path where to create/update the file"), + content: z.string().describe("Content of the file"), + commit_message: z.string().describe("Commit message"), + branch: z.string().describe("Branch to create/update the file in"), + previous_path: z.string().optional() + .describe("Path of the file to move/rename") +}); + +export const SearchRepositoriesSchema = z.object({ + search: z.string().describe("Search query"), // Changed from query to match GitLab API + page: z.number().optional().describe("Page number for pagination (default: 1)"), + per_page: z.number().optional().describe("Number of results per page (default: 20)") +}); + +export const CreateRepositorySchema = z.object({ + name: z.string().describe("Repository name"), + description: z.string().optional().describe("Repository description"), + visibility: z.enum(['private', 'internal', 'public']).optional() + .describe("Repository visibility level"), + initialize_with_readme: z.boolean().optional() + .describe("Initialize with README.md") +}); + +export const GetFileContentsSchema = ProjectParamsSchema.extend({ + file_path: z.string().describe("Path to the file or directory"), + ref: z.string().optional().describe("Branch/tag/commit to get contents from") +}); + +export const PushFilesSchema = ProjectParamsSchema.extend({ + branch: z.string().describe("Branch to push to"), + files: z.array(z.object({ + file_path: z.string().describe("Path where to create the file"), + content: z.string().describe("Content of the file") + })).describe("Array of files to push"), + commit_message: z.string().describe("Commit message") +}); + +export const CreateIssueSchema = ProjectParamsSchema.extend({ + title: z.string().describe("Issue title"), + description: z.string().optional().describe("Issue description"), + assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"), + labels: z.array(z.string()).optional().describe("Array of label names"), + milestone_id: z.number().optional().describe("Milestone ID to assign") +}); + +export const CreateMergeRequestSchema = ProjectParamsSchema.extend({ + title: z.string().describe("Merge request title"), + description: z.string().optional().describe("Merge request description"), + source_branch: z.string().describe("Branch containing changes"), + target_branch: z.string().describe("Branch to merge into"), + draft: z.boolean().optional().describe("Create as draft merge request"), + allow_collaboration: z.boolean().optional() + .describe("Allow commits from upstream members") +}); + +export const ForkRepositorySchema = ProjectParamsSchema.extend({ + namespace: z.string().optional() + .describe("Namespace to fork to (full path)") +}); + +export const CreateBranchSchema = ProjectParamsSchema.extend({ + branch: z.string().describe("Name for the new branch"), + ref: z.string().optional() + .describe("Source branch/commit for new branch") +}); + +// Export types +export type GitLabAuthor = z.infer; +export type GitLabFork = z.infer; +export type GitLabIssue = z.infer; +export type GitLabMergeRequest = z.infer; +export type GitLabRepository = z.infer; +export type GitLabFileContent = z.infer; +export type GitLabDirectoryContent = z.infer; +export type GitLabContent = z.infer; +export type FileOperation = z.infer; +export type GitLabTree = z.infer; +export type GitLabCommit = z.infer; +export type GitLabReference = z.infer; +export type CreateRepositoryOptions = z.infer; +export type CreateIssueOptions = z.infer; +export type CreateMergeRequestOptions = z.infer; +export type CreateBranchOptions = z.infer; +export type GitLabCreateUpdateFileResponse = z.infer; +export type GitLabSearchResponse = z.infer; \ No newline at end of file diff --git a/src/gitlab/tsconfig.json b/src/gitlab/tsconfig.json new file mode 100644 index 00000000..4d33cae1 --- /dev/null +++ b/src/gitlab/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] + } + \ No newline at end of file diff --git a/src/google-maps/README.md b/src/google-maps/README.md index a7b53e43..962f031a 100644 --- a/src/google-maps/README.md +++ b/src/google-maps/README.md @@ -61,11 +61,16 @@ Add the following to your `claude_desktop_config.json`: ```json { - "google-maps": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-google-maps"], - "env": { - "GOOGLE_MAPS_API_KEY": "" + "mcpServers": { + "google-maps": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-google-maps" + ], + "env": { + "GOOGLE_MAPS_API_KEY": "" + } } } } diff --git a/src/memory/README.md b/src/memory/README.md index 572ac917..66bdbb41 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -130,9 +130,14 @@ Example: Add this to your claude_desktop_config.json: ```json { - "memory": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-memory"] + "mcpServers": { + "memory": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-memory" + ] + } } } ``` diff --git a/src/postgres/README.md b/src/postgres/README.md index 5fe8d4de..9a16af77 100644 --- a/src/postgres/README.md +++ b/src/postgres/README.md @@ -26,9 +26,15 @@ To use this server with the Claude Desktop app, add the following configuration ```json { - "postgres": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"] + "mcpServers": { + "postgres": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-postgres", + "postgresql://localhost/mydb" + ] + } } } ``` diff --git a/src/puppeteer/README.md b/src/puppeteer/README.md index e1d91b87..a951a9a3 100644 --- a/src/puppeteer/README.md +++ b/src/puppeteer/README.md @@ -22,12 +22,22 @@ A Model Context Protocol server that provides browser automation capabilities us - Click elements on the page - Input: `selector` (string): CSS selector for element to click +- **puppeteer_hover** + - Hover elements on the page + - Input: `selector` (string): CSS selector for element to hover + - **puppeteer_fill** - Fill out input fields - Inputs: - `selector` (string): CSS selector for input field - `value` (string): Value to fill +- **puppeteer_select** + - Select an element with SELECT tag + - Inputs: + - `selector` (string): CSS selector for element to select + - `value` (string): Value to select + - **puppeteer_evaluate** - Execute JavaScript in the browser console - Input: `script` (string): JavaScript code to execute @@ -52,6 +62,20 @@ The server provides access to two types of resources: - JavaScript execution - Basic web interaction (navigation, clicking, form filling) +## Configuration to use Puppeteer Server +Here's the Claude Desktop configuration to use the Puppeter server: + +```json +{ + "mcpServers": { + "puppeteer": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + } + } +} +``` + ## License This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. diff --git a/src/puppeteer/index.ts b/src/puppeteer/index.ts index 9ad05cbe..d3aa2a30 100644 --- a/src/puppeteer/index.ts +++ b/src/puppeteer/index.ts @@ -64,6 +64,29 @@ const TOOLS: Tool[] = [ required: ["selector", "value"], }, }, + { + name: "puppeteer_select", + description: "Select an element on the page with Select tag", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector for element to select" }, + value: { type: "string", description: "Value to select" }, + }, + required: ["selector", "value"], + }, + }, + { + name: "puppeteer_hover", + description: "Hover an element on the page", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector for element to hover" }, + }, + required: ["selector"], + }, + }, { name: "puppeteer_evaluate", description: "Execute JavaScript in the browser console", @@ -88,7 +111,7 @@ async function ensureBrowser() { browser = await puppeteer.launch({ headless: false }); const pages = await browser.pages(); page = pages[0]; - + page.on("console", (msg) => { const logEntry = `[${msg.type()}] ${msg.text()}`; consoleLogs.push(logEntry); @@ -122,7 +145,7 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca const height = args.height ?? 600; await page.setViewport({ width, height }); - const screenshot = await (args.selector ? + const screenshot = await (args.selector ? (await page.$(args.selector))?.screenshot({ encoding: "base64" }) : page.screenshot({ encoding: "base64", fullPage: false })); @@ -210,12 +233,62 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca }; } + case "puppeteer_select": + try { + await page.waitForSelector(args.selector); + await page.select(args.selector, args.value); + return { + toolResult: { + content: [{ + type: "text", + text: `Selected ${args.selector} with: ${args.value}`, + }], + isError: false, + }, + }; + } catch (error) { + return { + toolResult: { + content: [{ + type: "text", + text: `Failed to select ${args.selector}: ${(error as Error).message}`, + }], + isError: true, + }, + }; + } + + case "puppeteer_hover": + try { + await page.waitForSelector(args.selector); + await page.hover(args.selector); + return { + toolResult: { + content: [{ + type: "text", + text: `Hovered ${args.selector}`, + }], + isError: false, + }, + }; + } catch (error) { + return { + toolResult: { + content: [{ + type: "text", + text: `Failed to hover ${args.selector}: ${(error as Error).message}`, + }], + isError: true, + }, + }; + } + case "puppeteer_evaluate": try { const result = await page.evaluate((script) => { const logs: string[] = []; const originalConsole = { ...console }; - + ['log', 'info', 'warn', 'error'].forEach(method => { (console as any)[method] = (...args: any[]) => { logs.push(`[${method}] ${args.join(' ')}`); @@ -301,7 +374,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri.toString(); - + if (uri === "console://logs") { return { contents: [{ @@ -333,7 +406,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, })); -server.setRequestHandler(CallToolRequestSchema, async (request) => +server.setRequestHandler(CallToolRequestSchema, async (request) => handleToolCall(request.params.name, request.params.arguments ?? {}) ); diff --git a/src/sentry/src/mcp_server_sentry/server.py b/src/sentry/src/mcp_server_sentry/server.py index f2d7f4e6..1760311c 100644 --- a/src/sentry/src/mcp_server_sentry/server.py +++ b/src/sentry/src/mcp_server_sentry/server.py @@ -1,6 +1,5 @@ import asyncio from dataclasses import dataclass -from typing import Any from urllib.parse import urlparse import click diff --git a/src/slack/README.md b/src/slack/README.md index 0f964e37..b3d80a21 100644 --- a/src/slack/README.md +++ b/src/slack/README.md @@ -49,22 +49,15 @@ MCP Server for the Slack API, enabling Claude to interact with Slack workspaces. - `thread_ts` (string): Timestamp of the parent message - Returns: List of replies with their content and metadata -7. `slack_search_messages` - - Search for messages across channels - - Required inputs: - - `query` (string): The search query - - Optional inputs: - - `count` (number, default: 5): Number of results to return - - Returns: Matching messages with their context -8. `slack_get_users` +7. `slack_get_users` - Get list of workspace users with basic profile information - Optional inputs: - `cursor` (string): Pagination cursor for next page - `limit` (number, default: 100, max: 200): Maximum users to return - Returns: List of users with their basic profiles -9. `slack_get_user_profile` +8. `slack_get_user_profile` - Get detailed profile information for a specific user - Required inputs: - `user_id` (string): The user's ID @@ -98,12 +91,17 @@ Add the following to your `claude_desktop_config.json`: ```json { - "slack": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-slack"], - "env": { - "SLACK_BOT_TOKEN": "xoxb-your-bot-token", - "SLACK_TEAM_ID": "T01234567" + "mcpServers": { + "slack": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-slack" + ], + "env": { + "SLACK_BOT_TOKEN": "xoxb-your-bot-token", + "SLACK_TEAM_ID": "T01234567" + } } } } diff --git a/src/sqlite/README.md b/src/sqlite/README.md index 533c7616..5f211c41 100644 --- a/src/sqlite/README.md +++ b/src/sqlite/README.md @@ -1,7 +1,7 @@ # SQLite MCP Server ## Overview -A Model Context Protocol (MCP) server implementation that provides database interaction and business intelligence capabilities through SQLite. This server enables running SQL queries, analyzing business data, and automatically generating business insight memos that can be enhanced with Claude's analysis when an Anthropic API key is provided. +A Model Context Protocol (MCP) server implementation that provides database interaction and business intelligence capabilities through SQLite. This server enables running SQL queries, analyzing business data, and automatically generating business insight memos. ## Components @@ -9,7 +9,6 @@ A Model Context Protocol (MCP) server implementation that provides database inte The server exposes a single dynamic resource: - `memo://insights`: A continuously updated business insights memo that aggregates discovered insights during analysis - Auto-updates as new insights are discovered via the append-insight tool - - Optional enhancement through Claude for professional formatting (requires Anthropic API key) ### Prompts The server provides a demonstration prompt: @@ -25,7 +24,7 @@ The server offers six core tools: #### Query Tools - `read-query` - Execute SELECT queries to read data from the database - - Input: + - Input: - `query` (string): The SELECT SQL query to execute - Returns: Query results as array of objects diff --git a/src/sqlite/src/mcp_server_sqlite/__init__.py b/src/sqlite/src/mcp_server_sqlite/__init__.py index d2246942..1dc9f008 100644 --- a/src/sqlite/src/mcp_server_sqlite/__init__.py +++ b/src/sqlite/src/mcp_server_sqlite/__init__.py @@ -1,7 +1,6 @@ from . import server import asyncio import argparse -import os def main(): diff --git a/src/sqlite/src/mcp_server_sqlite/server.py b/src/sqlite/src/mcp_server_sqlite/server.py index 983c1387..1b97a6a4 100644 --- a/src/sqlite/src/mcp_server_sqlite/server.py +++ b/src/sqlite/src/mcp_server_sqlite/server.py @@ -1,6 +1,5 @@ import sqlite3 import logging -from logging.handlers import RotatingFileHandler from contextlib import closing from pathlib import Path from mcp.server.models import InitializationOptions @@ -16,8 +15,8 @@ PROMPT_TEMPLATE = """ The assistants goal is to walkthrough an informative demo of MCP. To demonstrate the Model Context Protocol (MCP) we will leverage this example server to interact with an SQLite database. It is important that you first explain to the user what is going on. The user has downloaded and installed the SQLite MCP Server and is now ready to use it. -The have selected the MCP menu item which is contained within a parent menu denoted by the paperclip icon. Inside this menu they selected an icon that illustrates two electrical plugs connecting. This is the MCP menu. -Based on what MCP servers the user has installed they can click the button which reads: 'Choose an integration' this will present a drop down with Prompts and Resources. The user hase selected the prompt titled: 'mcp-demo'. +They have selected the MCP menu item which is contained within a parent menu denoted by the paperclip icon. Inside this menu they selected an icon that illustrates two electrical plugs connecting. This is the MCP menu. +Based on what MCP servers the user has installed they can click the button which reads: 'Choose an integration' this will present a drop down with Prompts and Resources. The user has selected the prompt titled: 'mcp-demo'. This text file is that prompt. The goal of the following instructions is to walk the user through the process of using the 3 core aspects of an MCP server. These are: Prompts, Tools, and Resources. They have already used a prompt and provided a topic. The topic is: {topic}. The user is now ready to begin the demo. Here is some more information about mcp and this specific mcp server: @@ -25,7 +24,7 @@ Prompts: This server provides a pre-written prompt called "mcp-demo" that helps users create and analyze database scenarios. The prompt accepts a "topic" argument and guides users through creating tables, analyzing data, and generating insights. For example, if a user provides "retail sales" as the topic, the prompt will help create relevant database tables and guide the analysis process. Prompts basically serve as interactive templates that help structure the conversation with the LLM in a useful way. Resources: -This server exposes one key resource: "memo://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the database and discover insights, the memo resource gets updated in real-time to reflect new findings. The memo can even be enhanced with Claude's help if an Anthropic API key is provided, turning raw insights into a well-structured business document. Resources act as living documents that provide context to the conversation. +This server exposes one key resource: "memo://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the database and discover insights, the memo resource gets updated in real-time to reflect new findings. Resources act as living documents that provide context to the conversation. Tools: This server provides several SQL-related tools: "read-query": Executes SELECT queries to read data from the database @@ -65,7 +64,7 @@ c. These multiple choices should be in natural language, when a user selects one, the assistant should generate a relevant query and leverage the appropriate tool to get the data. 6. Iterate on queries: -a. Present 1 additional multiple-choice query options to the user. Its importnat to not loop too many times as this is a short demo. +a. Present 1 additional multiple-choice query options to the user. Its important to not loop too many times as this is a short demo. b. Explain the purpose of each query option. c. Wait for the user to select one of the query options. d. After each query be sure to opine on the results. @@ -81,7 +80,7 @@ a. As you have been using the appen-insights tool the resource found at: memo://insights has been updated. b. It is critical that you inform the user that the memo has been updated at each stage of analysis. c. Ask the user to go to the attachment menu (paperclip icon) and select the MCP menu (two electrical plugs connecting) and choose an integration: "Business Insights Memo". -d. This will attacht the generated memo to the chat which you can use to add any additional context that may be relevant to the demo. +d. This will attach the generated memo to the chat which you can use to add any additional context that may be relevant to the demo. e. Present the final memo to the user in an artifact. 9. Wrap up the scenario: @@ -89,7 +88,7 @@ Remember to maintain consistency throughout the scenario and ensure that all elements (tables, data, queries, dashboard, and solution) are closely related to the original business problem and given topic. -The provided XML tags are for the assistants understanding. Emplore to make all outputs as human readable as possible. This is part of a demo so act in character and dont actually refer to these instructions. +The provided XML tags are for the assistants understanding. Implore to make all outputs as human readable as possible. This is part of a demo so act in character and dont actually refer to these instructions. Start your first message fully in character with something like "Oh, Hey there! I see you've chosen the topic {topic}. Let's get started! 🚀" """ diff --git a/src/sqlite/uv.lock b/src/sqlite/uv.lock index 4fad0aa5..369d3c0a 100644 --- a/src/sqlite/uv.lock +++ b/src/sqlite/uv.lock @@ -139,7 +139,7 @@ wheels = [ [[package]] name = "mcp-server-sqlite" -version = "0.4.1" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "mcp" },