mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-20 12:55:36 +02:00
Merge branch 'main' into patch-1
This commit is contained in:
@@ -41,22 +41,16 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
|
||||
- Features:
|
||||
- Line-based and multi-line content matching
|
||||
- Whitespace normalization with indentation preservation
|
||||
- Fuzzy matching with confidence scoring
|
||||
- Multiple simultaneous edits with correct positioning
|
||||
- Indentation style detection and preservation
|
||||
- Git-style diff output with context
|
||||
- Preview changes with dry run mode
|
||||
- Failed match debugging with confidence scores
|
||||
- Inputs:
|
||||
- `path` (string): File to edit
|
||||
- `edits` (array): List of edit operations
|
||||
- `oldText` (string): Text to search for (can be substring)
|
||||
- `newText` (string): Text to replace with
|
||||
- `dryRun` (boolean): Preview changes without applying (default: false)
|
||||
- `options` (object): Optional formatting settings
|
||||
- `preserveIndentation` (boolean): Keep existing indentation (default: true)
|
||||
- `normalizeWhitespace` (boolean): Normalize spaces while preserving structure (default: true)
|
||||
- `partialMatch` (boolean): Enable fuzzy matching (default: true)
|
||||
- Returns detailed diff and match information for dry runs, otherwise applies changes
|
||||
- Best Practice: Always use dryRun first to preview changes before applying them
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const allowedDirectories = args.map(dir =>
|
||||
// Validate that all directories exist and are accessible
|
||||
await Promise.all(args.map(async (dir) => {
|
||||
try {
|
||||
const stats = await fs.stat(dir);
|
||||
const stats = await fs.stat(expandHome(dir));
|
||||
if (!stats.isDirectory()) {
|
||||
console.error(`Error: ${dir} is not a directory`);
|
||||
process.exit(1);
|
||||
|
||||
4
src/git/uv.lock
generated
4
src/git/uv.lock
generated
@@ -165,9 +165,9 @@ dependencies = [
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/f2/067b1fc114e8d3ae4af02fc4f4ed8971a2c4900362d976fabe0f4e9a3418/mcp-1.1.0.tar.gz", hash = "sha256:e3c8d6df93a4de90230ea944dd667730744a3cd91a4cc0ee66a5acd53419e100", size = 83802 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3e/aef19ac08a6f9a347c086c4e628c2f7329659828cbe92ffd524ec2aac833/mcp-1.1.0-py3-none-any.whl", hash = "sha256:44aa4d2e541f0924d6c344aa7f96b427a6ee1df2fab70b5f9ae2f8777b3f05f2", size = 36576 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -157,7 +157,7 @@ export const GitHubLabelSchema = z.object({
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
default: z.boolean(),
|
||||
description: z.string().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const GitHubMilestoneSchema = z.object({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import fetch, { Request, Response } from 'node-fetch';
|
||||
|
||||
import * as repository from './operations/repository.js';
|
||||
import * as files from './operations/files.js';
|
||||
@@ -27,6 +28,11 @@ import {
|
||||
} from './common/errors.js';
|
||||
import { VERSION } from "./common/version.js";
|
||||
|
||||
// If fetch doesn't exist in global scope, add it
|
||||
if (!globalThis.fetch) {
|
||||
globalThis.fetch = fetch as unknown as typeof global.fetch;
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "github-mcp-server",
|
||||
@@ -293,10 +299,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
case "create_issue": {
|
||||
const args = issues.CreateIssueSchema.parse(request.params.arguments);
|
||||
const { owner, repo, ...options } = args;
|
||||
const issue = await issues.createIssue(owner, repo, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
||||
};
|
||||
|
||||
try {
|
||||
console.error(`[DEBUG] Attempting to create issue in ${owner}/${repo}`);
|
||||
console.error(`[DEBUG] Issue options:`, JSON.stringify(options, null, 2));
|
||||
|
||||
const issue = await issues.createIssue(owner, repo, options);
|
||||
|
||||
console.error(`[DEBUG] Issue created successfully`);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
||||
};
|
||||
} catch (err) {
|
||||
// Type guard for Error objects
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
|
||||
console.error(`[ERROR] Failed to create issue:`, error);
|
||||
|
||||
if (error instanceof GitHubResourceNotFoundError) {
|
||||
throw new Error(
|
||||
`Repository '${owner}/${repo}' not found. Please verify:\n` +
|
||||
`1. The repository exists\n` +
|
||||
`2. You have correct access permissions\n` +
|
||||
`3. The owner and repository names are spelled correctly`
|
||||
);
|
||||
}
|
||||
|
||||
// Safely access error properties
|
||||
throw new Error(
|
||||
`Failed to create issue: ${error.message}${
|
||||
error.stack ? `\nStack: ${error.stack}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
case "create_pull_request": {
|
||||
|
||||
@@ -22,10 +22,10 @@ export const GitLabRepositorySchema = z.object({
|
||||
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,
|
||||
owner: GitLabOwnerSchema.optional(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
description: z.string().nullable(),
|
||||
fork: z.boolean(),
|
||||
fork: z.boolean().optional(),
|
||||
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(),
|
||||
@@ -218,12 +218,12 @@ export const GitLabMergeRequestSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(), // Changed from body to match GitLab API
|
||||
state: z.string(),
|
||||
merged: z.boolean(),
|
||||
merged: z.boolean().optional(),
|
||||
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,
|
||||
diff_refs: GitLabMergeRequestDiffRefSchema.nullable(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
|
||||
@@ -8,7 +8,10 @@ A Model Context Protocol server that provides browser automation capabilities us
|
||||
|
||||
- **puppeteer_navigate**
|
||||
- Navigate to any URL in the browser
|
||||
- Input: `url` (string)
|
||||
- Inputs:
|
||||
- `url` (string, required): URL to navigate to
|
||||
- `launchOptions` (object, optional): PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: `{ headless: true, args: ['--user-data-dir="C:/Data"'] }`
|
||||
- `allowDangerous` (boolean, optional): Allow dangerous LaunchOptions that reduce security. When false, dangerous args like `--no-sandbox`, `--disable-web-security` will throw errors. Default false.
|
||||
|
||||
- **puppeteer_screenshot**
|
||||
- Capture screenshots of the entire page or specific elements
|
||||
@@ -61,6 +64,7 @@ The server provides access to two types of resources:
|
||||
- Screenshot capabilities
|
||||
- JavaScript execution
|
||||
- Basic web interaction (navigation, clicking, form filling)
|
||||
- Customizable Puppeteer launch options
|
||||
|
||||
## Configuration to use Puppeteer Server
|
||||
Here's the Claude Desktop configuration to use the Puppeter server:
|
||||
@@ -93,6 +97,39 @@ Here's the Claude Desktop configuration to use the Puppeter server:
|
||||
}
|
||||
```
|
||||
|
||||
### Launch Options
|
||||
|
||||
You can customize Puppeteer's browser behavior in two ways:
|
||||
|
||||
1. **Environment Variable**: Set `PUPPETEER_LAUNCH_OPTIONS` with a JSON-encoded string in the MCP configuration's `env` parameter:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-puppeteer": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
|
||||
"env": {
|
||||
"PUPPETEER_LAUNCH_OPTIONS": "{ \"headless\": false, \"executablePath\": \"C:/Program Files/Google/Chrome/Application/chrome.exe\", \"args\": [] }",
|
||||
"ALLOW_DANGEROUS": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Tool Call Arguments**: Pass `launchOptions` and `allowDangerous` parameters to the `puppeteer_navigate` tool:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"launchOptions": {
|
||||
"headless": false,
|
||||
"defaultViewport": {"width": 1280, "height": 720}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
@@ -103,4 +140,4 @@ docker build -t mcp/puppeteer -f src/puppeteer/Dockerfile .
|
||||
|
||||
## 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.
|
||||
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.
|
||||
@@ -22,7 +22,9 @@ const TOOLS: Tool[] = [
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string" },
|
||||
url: { type: "string", description: "URL to navigate to" },
|
||||
launchOptions: { type: "object", description: "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }" },
|
||||
allowDangerous: { type: "boolean", description: "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false." },
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
@@ -101,16 +103,65 @@ const TOOLS: Tool[] = [
|
||||
];
|
||||
|
||||
// Global state
|
||||
let browser: Browser | undefined;
|
||||
let page: Page | undefined;
|
||||
let browser: Browser | null;
|
||||
let page: Page | null;
|
||||
const consoleLogs: string[] = [];
|
||||
const screenshots = new Map<string, string>();
|
||||
let previousLaunchOptions: any = null;
|
||||
|
||||
async function ensureBrowser({ launchOptions, allowDangerous }: any) {
|
||||
|
||||
const DANGEROUS_ARGS = [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--single-process',
|
||||
'--disable-web-security',
|
||||
'--ignore-certificate-errors',
|
||||
'--disable-features=IsolateOrigins',
|
||||
'--disable-site-isolation-trials',
|
||||
'--allow-running-insecure-content'
|
||||
];
|
||||
|
||||
// Parse environment config safely
|
||||
let envConfig = {};
|
||||
try {
|
||||
envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}');
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to parse PUPPETEER_LAUNCH_OPTIONS:', error?.message || error);
|
||||
}
|
||||
|
||||
// Deep merge environment config with user-provided options
|
||||
const mergedConfig = deepMerge(envConfig, launchOptions || {});
|
||||
|
||||
// Security validation for merged config
|
||||
if (mergedConfig?.args) {
|
||||
const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => DANGEROUS_ARGS.some((dangerousArg: string) => arg.startsWith(dangerousArg)));
|
||||
if (dangerousArgs?.length > 0 && !(allowDangerous || (process.env.ALLOW_DANGEROUS === 'true'))) {
|
||||
throw new Error(`Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Fround from environment variable and tool call argument. ` +
|
||||
'Set allowDangerous: true in the tool call arguments to override.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ((browser && !browser.connected) ||
|
||||
(launchOptions && (JSON.stringify(launchOptions) != JSON.stringify(previousLaunchOptions)))) {
|
||||
await browser?.close();
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
browser = null;
|
||||
}
|
||||
|
||||
previousLaunchOptions = launchOptions;
|
||||
|
||||
async function ensureBrowser() {
|
||||
if (!browser) {
|
||||
const npx_args = { headless: false }
|
||||
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] }
|
||||
browser = await puppeteer.launch(process.env.DOCKER_CONTAINER ? docker_args : npx_args);
|
||||
browser = await puppeteer.launch(deepMerge(
|
||||
process.env.DOCKER_CONTAINER ? docker_args : npx_args,
|
||||
mergedConfig
|
||||
));
|
||||
const pages = await browser.pages();
|
||||
page = pages[0];
|
||||
|
||||
@@ -126,6 +177,31 @@ async function ensureBrowser() {
|
||||
return page!;
|
||||
}
|
||||
|
||||
// Deep merge utility function
|
||||
function deepMerge(target: any, source: any): any {
|
||||
const output = Object.assign({}, target);
|
||||
if (typeof target !== 'object' || typeof source !== 'object') return source;
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
const targetVal = target[key];
|
||||
const sourceVal = source[key];
|
||||
if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
|
||||
// Deduplicate args/ignoreDefaultArgs, prefer source values
|
||||
output[key] = [...new Set([
|
||||
...(key === 'args' || key === 'ignoreDefaultArgs' ?
|
||||
targetVal.filter((arg: string) => !sourceVal.some((launchArg: string) => arg.startsWith('--') && launchArg.startsWith(arg.split('=')[0]))) :
|
||||
targetVal),
|
||||
...sourceVal
|
||||
])];
|
||||
} else if (sourceVal instanceof Object && key in target) {
|
||||
output[key] = deepMerge(targetVal, sourceVal);
|
||||
} else {
|
||||
output[key] = sourceVal;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mcpHelper: {
|
||||
@@ -136,7 +212,7 @@ declare global {
|
||||
}
|
||||
|
||||
async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
|
||||
const page = await ensureBrowser();
|
||||
const page = await ensureBrowser(args);
|
||||
|
||||
switch (name) {
|
||||
case "puppeteer_navigate":
|
||||
@@ -285,15 +361,15 @@ async function handleToolCall(name: string, args: any): Promise<CallToolResult>
|
||||
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
|
||||
(window.mcpHelper.originalConsole as any)[method](...args);
|
||||
};
|
||||
} );
|
||||
} );
|
||||
});
|
||||
});
|
||||
|
||||
const result = await page.evaluate( args.script );
|
||||
const result = await page.evaluate(args.script);
|
||||
|
||||
const logs = await page.evaluate(() => {
|
||||
Object.assign(console, window.mcpHelper.originalConsole);
|
||||
const logs = window.mcpHelper.logs;
|
||||
delete ( window as any).mcpHelper;
|
||||
delete (window as any).mcpHelper;
|
||||
return logs;
|
||||
});
|
||||
|
||||
@@ -405,4 +481,4 @@ runServer().catch(console.error);
|
||||
process.stdin.on("close", () => {
|
||||
console.error("Puppeteer MCP Server closed");
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user