mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-20 12:55:36 +02:00
Merge remote-tracking branch 'upstream/main' into burkeholland-vscode-install-instructions
This commit is contained in:
20
src/everything/CLAUDE.md
Normal file
20
src/everything/CLAUDE.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# MCP "Everything" Server - Development Guidelines
|
||||
|
||||
## Build, Test & Run Commands
|
||||
- Build: `npm run build` - Compiles TypeScript to JavaScript
|
||||
- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically
|
||||
- Run server: `npm run start` - Starts the MCP server using stdio transport
|
||||
- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport
|
||||
- Prepare release: `npm run prepare` - Builds the project for publishing
|
||||
|
||||
## Code Style Guidelines
|
||||
- Use ES modules with `.js` extension in import paths
|
||||
- Strictly type all functions and variables with TypeScript
|
||||
- Follow zod schema patterns for tool input validation
|
||||
- Prefer async/await over callbacks and Promise chains
|
||||
- Place all imports at top of file, grouped by external then internal
|
||||
- Use descriptive variable names that clearly indicate purpose
|
||||
- Implement proper cleanup for timers and resources in server shutdown
|
||||
- Follow camelCase for variables/functions, PascalCase for types/classes, UPPER_CASE for constants
|
||||
- Handle errors with try/catch blocks and provide clear error messages
|
||||
- Use consistent indentation (2 spaces) and trailing commas in multi-line objects
|
||||
@@ -63,6 +63,15 @@ This MCP server attempts to exercise all the features of the MCP protocol. It is
|
||||
}
|
||||
```
|
||||
|
||||
8. `getResourceReference`
|
||||
- Returns a resource reference that can be used by MCP clients
|
||||
- Inputs:
|
||||
- `resourceId` (number, 1-100): ID of the resource to reference
|
||||
- Returns: A resource reference with:
|
||||
- Text introduction
|
||||
- Embedded resource with `type: "resource"`
|
||||
- Text instruction for using the resource URI
|
||||
|
||||
### Resources
|
||||
|
||||
The server provides 100 test resources in two formats:
|
||||
@@ -96,6 +105,13 @@ Resource features:
|
||||
- `style` (string): Output style preference
|
||||
- Returns: Multi-turn conversation with images
|
||||
|
||||
3. `resource_prompt`
|
||||
- Demonstrates embedding resource references in prompts
|
||||
- Required arguments:
|
||||
- `resourceId` (number): ID of the resource to embed (1-100)
|
||||
- Returns: Multi-turn conversation with an embedded resource reference
|
||||
- Shows how to include resources directly in prompt messages
|
||||
|
||||
### Logging
|
||||
|
||||
The server sends random-leveled log messages every 15 seconds, e.g.:
|
||||
|
||||
@@ -62,10 +62,21 @@ const EXAMPLE_COMPLETIONS = {
|
||||
const GetTinyImageSchema = z.object({});
|
||||
|
||||
const AnnotatedMessageSchema = z.object({
|
||||
messageType: z.enum(["error", "success", "debug"])
|
||||
messageType: z
|
||||
.enum(["error", "success", "debug"])
|
||||
.describe("Type of message to demonstrate different annotation patterns"),
|
||||
includeImage: z.boolean().default(false)
|
||||
.describe("Whether to include an example image")
|
||||
includeImage: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe("Whether to include an example image"),
|
||||
});
|
||||
|
||||
const GetResourceReferenceSchema = z.object({
|
||||
resourceId: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.describe("ID of the resource to reference (1-100)"),
|
||||
});
|
||||
|
||||
enum ToolName {
|
||||
@@ -76,11 +87,13 @@ enum ToolName {
|
||||
SAMPLE_LLM = "sampleLLM",
|
||||
GET_TINY_IMAGE = "getTinyImage",
|
||||
ANNOTATED_MESSAGE = "annotatedMessage",
|
||||
GET_RESOURCE_REFERENCE = "getResourceReference",
|
||||
}
|
||||
|
||||
enum PromptName {
|
||||
SIMPLE = "simple_prompt",
|
||||
COMPLEX = "complex_prompt",
|
||||
RESOURCE = "resource_prompt",
|
||||
}
|
||||
|
||||
export const createServer = () => {
|
||||
@@ -96,7 +109,7 @@ export const createServer = () => {
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let subscriptions: Set<string> = new Set();
|
||||
@@ -115,36 +128,37 @@ export const createServer = () => {
|
||||
let logLevel: LoggingLevel = "debug";
|
||||
let logsUpdateInterval: NodeJS.Timeout | undefined;
|
||||
const messages = [
|
||||
{level: "debug", data: "Debug-level message"},
|
||||
{level: "info", data: "Info-level message"},
|
||||
{level: "notice", data: "Notice-level message"},
|
||||
{level: "warning", data: "Warning-level message"},
|
||||
{level: "error", data: "Error-level message"},
|
||||
{level: "critical", data: "Critical-level message"},
|
||||
{level: "alert", data: "Alert level-message"},
|
||||
{level: "emergency", data: "Emergency-level message"}
|
||||
]
|
||||
{ level: "debug", data: "Debug-level message" },
|
||||
{ level: "info", data: "Info-level message" },
|
||||
{ level: "notice", data: "Notice-level message" },
|
||||
{ level: "warning", data: "Warning-level message" },
|
||||
{ level: "error", data: "Error-level message" },
|
||||
{ level: "critical", data: "Critical-level message" },
|
||||
{ level: "alert", data: "Alert level-message" },
|
||||
{ level: "emergency", data: "Emergency-level message" },
|
||||
];
|
||||
|
||||
const isMessageIgnored = (level:LoggingLevel):boolean => {
|
||||
const isMessageIgnored = (level: LoggingLevel): boolean => {
|
||||
const currentLevel = messages.findIndex((msg) => logLevel === msg.level);
|
||||
const messageLevel = messages.findIndex((msg) => level === msg.level);
|
||||
const messageLevel = messages.findIndex((msg) => level === msg.level);
|
||||
return messageLevel < currentLevel;
|
||||
}
|
||||
};
|
||||
|
||||
// Set up update interval for random log messages
|
||||
logsUpdateInterval = setInterval(() => {
|
||||
let message = {
|
||||
method: "notifications/message",
|
||||
params: messages[Math.floor(Math.random() * messages.length)],
|
||||
}
|
||||
if (!isMessageIgnored(message.params.level as LoggingLevel)) server.notification(message);
|
||||
};
|
||||
if (!isMessageIgnored(message.params.level as LoggingLevel))
|
||||
server.notification(message);
|
||||
}, 15000);
|
||||
|
||||
// Helper method to request sampling from client
|
||||
const requestSampling = async (
|
||||
context: string,
|
||||
uri: string,
|
||||
maxTokens: number = 100,
|
||||
maxTokens: number = 100
|
||||
) => {
|
||||
const request: CreateMessageRequest = {
|
||||
method: "sampling/createMessage",
|
||||
@@ -280,6 +294,17 @@ export const createServer = () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: PromptName.RESOURCE,
|
||||
description: "A prompt that includes an embedded resource reference",
|
||||
arguments: [
|
||||
{
|
||||
name: "resourceId",
|
||||
description: "Resource ID to include (1-100)",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -330,6 +355,37 @@ export const createServer = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === PromptName.RESOURCE) {
|
||||
const resourceId = parseInt(args?.resourceId as string, 10);
|
||||
if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) {
|
||||
throw new Error(
|
||||
`Invalid resourceId: ${args?.resourceId}. Must be a number between 1 and 100.`
|
||||
);
|
||||
}
|
||||
|
||||
const resourceIndex = resourceId - 1;
|
||||
const resource = ALL_RESOURCES[resourceIndex];
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`,
|
||||
},
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "resource",
|
||||
resource: resource,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown prompt: ${name}`);
|
||||
});
|
||||
|
||||
@@ -347,7 +403,8 @@ export const createServer = () => {
|
||||
},
|
||||
{
|
||||
name: ToolName.PRINT_ENV,
|
||||
description: "Prints all environment variables, helpful for debugging MCP server configuration",
|
||||
description:
|
||||
"Prints all environment variables, helpful for debugging MCP server configuration",
|
||||
inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
@@ -368,9 +425,16 @@ export const createServer = () => {
|
||||
},
|
||||
{
|
||||
name: ToolName.ANNOTATED_MESSAGE,
|
||||
description: "Demonstrates how annotations can be used to provide metadata about content",
|
||||
description:
|
||||
"Demonstrates how annotations can be used to provide metadata about content",
|
||||
inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.GET_RESOURCE_REFERENCE,
|
||||
description:
|
||||
"Returns a resource reference that can be used by MCP clients",
|
||||
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
|
||||
},
|
||||
];
|
||||
|
||||
return { tools };
|
||||
@@ -407,7 +471,7 @@ export const createServer = () => {
|
||||
|
||||
for (let i = 1; i < steps + 1; i++) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, stepDuration * 1000),
|
||||
setTimeout(resolve, stepDuration * 1000)
|
||||
);
|
||||
|
||||
if (progressToken !== undefined) {
|
||||
@@ -450,10 +514,12 @@ export const createServer = () => {
|
||||
const result = await requestSampling(
|
||||
prompt,
|
||||
ToolName.SAMPLE_LLM,
|
||||
maxTokens,
|
||||
maxTokens
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: `LLM sampling result: ${result.content.text}` }],
|
||||
content: [
|
||||
{ type: "text", text: `LLM sampling result: ${result.content.text}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -478,6 +544,35 @@ export const createServer = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === ToolName.GET_RESOURCE_REFERENCE) {
|
||||
const validatedArgs = GetResourceReferenceSchema.parse(args);
|
||||
const resourceId = validatedArgs.resourceId;
|
||||
|
||||
const resourceIndex = resourceId - 1;
|
||||
if (resourceIndex < 0 || resourceIndex >= ALL_RESOURCES.length) {
|
||||
throw new Error(`Resource with ID ${resourceId} does not exist`);
|
||||
}
|
||||
|
||||
const resource = ALL_RESOURCES[resourceIndex];
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Returning resource reference for Resource ${resourceId}:`,
|
||||
},
|
||||
{
|
||||
type: "resource",
|
||||
resource: resource,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: `You can access this resource using the URI: ${resource.uri}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (name === ToolName.ANNOTATED_MESSAGE) {
|
||||
const { messageType, includeImage } = AnnotatedMessageSchema.parse(args);
|
||||
|
||||
@@ -490,8 +585,8 @@ export const createServer = () => {
|
||||
text: "Error: Operation failed",
|
||||
annotations: {
|
||||
priority: 1.0, // Errors are highest priority
|
||||
audience: ["user", "assistant"] // Both need to know about errors
|
||||
}
|
||||
audience: ["user", "assistant"], // Both need to know about errors
|
||||
},
|
||||
});
|
||||
} else if (messageType === "success") {
|
||||
content.push({
|
||||
@@ -499,8 +594,8 @@ export const createServer = () => {
|
||||
text: "Operation completed successfully",
|
||||
annotations: {
|
||||
priority: 0.7, // Success messages are important but not critical
|
||||
audience: ["user"] // Success mainly for user consumption
|
||||
}
|
||||
audience: ["user"], // Success mainly for user consumption
|
||||
},
|
||||
});
|
||||
} else if (messageType === "debug") {
|
||||
content.push({
|
||||
@@ -508,8 +603,8 @@ export const createServer = () => {
|
||||
text: "Debug: Cache hit ratio 0.95, latency 150ms",
|
||||
annotations: {
|
||||
priority: 0.3, // Debug info is low priority
|
||||
audience: ["assistant"] // Technical details for assistant
|
||||
}
|
||||
audience: ["assistant"], // Technical details for assistant
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -521,8 +616,8 @@ export const createServer = () => {
|
||||
mimeType: "image/png",
|
||||
annotations: {
|
||||
priority: 0.5,
|
||||
audience: ["user"] // Images primarily for user visualization
|
||||
}
|
||||
audience: ["user"], // Images primarily for user visualization
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -540,7 +635,7 @@ export const createServer = () => {
|
||||
if (!resourceId) return { completion: { values: [] } };
|
||||
|
||||
// Filter resource IDs that start with the input value
|
||||
const values = EXAMPLE_COMPLETIONS.resourceId.filter(id =>
|
||||
const values = EXAMPLE_COMPLETIONS.resourceId.filter((id) =>
|
||||
id.startsWith(argument.value)
|
||||
);
|
||||
return { completion: { values, hasMore: false, total: values.length } };
|
||||
@@ -548,10 +643,11 @@ export const createServer = () => {
|
||||
|
||||
if (ref.type === "ref/prompt") {
|
||||
// Handle completion for prompt arguments
|
||||
const completions = EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS];
|
||||
const completions =
|
||||
EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS];
|
||||
if (!completions) return { completion: { values: [] } };
|
||||
|
||||
const values = completions.filter(value =>
|
||||
const values = completions.filter((value) =>
|
||||
value.startsWith(argument.value)
|
||||
);
|
||||
return { completion: { values, hasMore: false, total: values.length } };
|
||||
|
||||
@@ -155,6 +155,10 @@ ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotoc
|
||||
|
||||
This can be customized by adding the argument `--user-agent=YourUserAgent` to the `args` list in the configuration.
|
||||
|
||||
### Customization - Proxy
|
||||
|
||||
The server can be configured to use a proxy by using the `--proxy-url` argument.
|
||||
|
||||
## Debugging
|
||||
|
||||
You can use the MCP inspector to debug the server. For uvx installations:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcp-server-fetch"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "A Model Context Protocol server providing tools to fetch and convert web content for usage by LLMs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -16,6 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx<0.28",
|
||||
"markdownify>=0.13.1",
|
||||
"mcp>=1.1.3",
|
||||
"protego>=0.3.1",
|
||||
|
||||
@@ -15,9 +15,10 @@ def main():
|
||||
action="store_true",
|
||||
help="Ignore robots.txt restrictions",
|
||||
)
|
||||
parser.add_argument("--proxy-url", type=str, help="Proxy URL to use for requests")
|
||||
|
||||
args = parser.parse_args()
|
||||
asyncio.run(serve(args.user_agent, args.ignore_robots_txt))
|
||||
asyncio.run(serve(args.user_agent, args.ignore_robots_txt, args.proxy_url))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -63,7 +63,7 @@ def get_robots_txt_url(url: str) -> str:
|
||||
return robots_url
|
||||
|
||||
|
||||
async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
|
||||
async def check_may_autonomously_fetch_url(url: str, user_agent: str, proxy_url: str | None = None) -> None:
|
||||
"""
|
||||
Check if the URL can be fetched by the user agent according to the robots.txt file.
|
||||
Raises a McpError if not.
|
||||
@@ -72,7 +72,7 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
|
||||
|
||||
robot_txt_url = get_robots_txt_url(url)
|
||||
|
||||
async with AsyncClient() as client:
|
||||
async with AsyncClient(proxies=proxy_url) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
robot_txt_url,
|
||||
@@ -109,14 +109,14 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
|
||||
|
||||
|
||||
async def fetch_url(
|
||||
url: str, user_agent: str, force_raw: bool = False
|
||||
url: str, user_agent: str, force_raw: bool = False, proxy_url: str | None = None
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.
|
||||
"""
|
||||
from httpx import AsyncClient, HTTPError
|
||||
|
||||
async with AsyncClient() as client:
|
||||
async with AsyncClient(proxies=proxy_url) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
url,
|
||||
@@ -173,19 +173,22 @@ class Fetch(BaseModel):
|
||||
bool,
|
||||
Field(
|
||||
default=False,
|
||||
description="Get the actual HTML content if the requested page, without simplification.",
|
||||
description="Get the actual HTML content of the requested page, without simplification.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def serve(
|
||||
custom_user_agent: str | None = None, ignore_robots_txt: bool = False
|
||||
custom_user_agent: str | None = None,
|
||||
ignore_robots_txt: bool = False,
|
||||
proxy_url: str | None = None,
|
||||
) -> None:
|
||||
"""Run the fetch MCP server.
|
||||
|
||||
Args:
|
||||
custom_user_agent: Optional custom User-Agent string to use for requests
|
||||
ignore_robots_txt: Whether to ignore robots.txt restrictions
|
||||
proxy_url: Optional proxy URL to use for requests
|
||||
"""
|
||||
server = Server("mcp-fetch")
|
||||
user_agent_autonomous = custom_user_agent or DEFAULT_USER_AGENT_AUTONOMOUS
|
||||
@@ -229,10 +232,10 @@ Although originally you did not have internet access, and were advised to refuse
|
||||
raise McpError(ErrorData(code=INVALID_PARAMS, message="URL is required"))
|
||||
|
||||
if not ignore_robots_txt:
|
||||
await check_may_autonomously_fetch_url(url, user_agent_autonomous)
|
||||
await check_may_autonomously_fetch_url(url, user_agent_autonomous, proxy_url)
|
||||
|
||||
content, prefix = await fetch_url(
|
||||
url, user_agent_autonomous, force_raw=args.raw
|
||||
url, user_agent_autonomous, force_raw=args.raw, proxy_url=proxy_url
|
||||
)
|
||||
original_length = len(content)
|
||||
if args.start_index >= original_length:
|
||||
@@ -259,7 +262,7 @@ Although originally you did not have internet access, and were advised to refuse
|
||||
url = arguments["url"]
|
||||
|
||||
try:
|
||||
content, prefix = await fetch_url(url, user_agent_manual)
|
||||
content, prefix = await fetch_url(url, user_agent_manual, proxy_url=proxy_url)
|
||||
# TODO: after SDK bug is addressed, don't catch the exception
|
||||
except McpError as e:
|
||||
return GetPromptResult(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# GitHub MCP Server
|
||||
|
||||
**Deprecation Notice:** Development for this project has been moved to GitHub in the http://github.com/github/github-mcp-server repo.
|
||||
|
||||
---
|
||||
|
||||
MCP Server for the GitHub API, enabling file operations, repository management, search functionality, and more.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -112,11 +112,20 @@ export const CreatePullRequestReviewSchema = z.object({
|
||||
commit_id: z.string().optional().describe("The SHA of the commit that needs a review"),
|
||||
body: z.string().describe("The body text of the review"),
|
||||
event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"),
|
||||
comments: z.array(z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
position: z.number().describe("The position in the diff where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
})).optional().describe("Comments to post as part of the review")
|
||||
comments: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
position: z.number().describe("The position in the diff where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
}),
|
||||
z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
line: z.number().describe("The line number in the file where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
})
|
||||
])
|
||||
).optional().describe("Comments to post as part of the review (specify either position or line, not both)")
|
||||
});
|
||||
|
||||
export const MergePullRequestSchema = z.object({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -148,6 +152,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:
|
||||
@@ -158,4 +195,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();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
A Model Context Protocol server that provides access to Redis databases. This server enables LLMs to interact with Redis key-value stores through a set of standardized tools.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Redis server must be installed and running
|
||||
- [Download Redis](https://redis.io/download)
|
||||
- For Windows users: Use [Windows Subsystem for Linux (WSL)](https://redis.io/docs/getting-started/installation/install-redis-on-windows/) or [Memurai](https://www.memurai.com/) (Redis-compatible Windows server)
|
||||
- Default port: 6379
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Connection Errors
|
||||
|
||||
**ECONNREFUSED**
|
||||
- **Cause**: Redis server is not running or unreachable
|
||||
- **Solution**:
|
||||
- Verify Redis is running: `redis-cli ping` should return "PONG"
|
||||
- Check Redis service status: `systemctl status redis` (Linux) or `brew services list` (macOS)
|
||||
- Ensure correct port (default 6379) is not blocked by firewall
|
||||
- Verify Redis URL format: `redis://hostname:port`
|
||||
|
||||
### Server Behavior
|
||||
|
||||
- The server implements exponential backoff with a maximum of 5 retries
|
||||
- Initial retry delay: 1 second, maximum delay: 30 seconds
|
||||
- Server will exit after max retries to prevent infinite reconnection loops
|
||||
|
||||
## Components
|
||||
|
||||
### Tools
|
||||
@@ -139,4 +164,4 @@ docker build -t mcp/redis -f src/redis/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.
|
||||
|
||||
@@ -7,10 +7,26 @@ import {
|
||||
import { z } from "zod";
|
||||
import { createClient } from 'redis';
|
||||
|
||||
// Get Redis URL from command line args or use default
|
||||
// Configuration
|
||||
const REDIS_URL = process.argv[2] || "redis://localhost:6379";
|
||||
const MAX_RETRIES = 5;
|
||||
const MIN_RETRY_DELAY = 1000; // 1 second
|
||||
const MAX_RETRY_DELAY = 30000; // 30 seconds
|
||||
|
||||
// Create Redis client with retry strategy
|
||||
const redisClient = createClient({
|
||||
url: REDIS_URL
|
||||
url: REDIS_URL,
|
||||
socket: {
|
||||
reconnectStrategy: (retries) => {
|
||||
if (retries >= MAX_RETRIES) {
|
||||
console.error(`Maximum retries (${MAX_RETRIES}) reached. Giving up.`);
|
||||
return new Error('Max retries reached');
|
||||
}
|
||||
const delay = Math.min(Math.pow(2, retries) * MIN_RETRY_DELAY, MAX_RETRY_DELAY);
|
||||
console.error(`Reconnection attempt ${retries + 1}/${MAX_RETRIES} in ${delay}ms`);
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Define Zod schemas for validation
|
||||
@@ -36,7 +52,12 @@ const ListArgumentsSchema = z.object({
|
||||
const server = new Server(
|
||||
{
|
||||
name: "redis",
|
||||
version: "1.0.0"
|
||||
version: "0.0.1"
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -215,22 +236,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
// Start the server
|
||||
async function main() {
|
||||
try {
|
||||
// Connect to Redis
|
||||
redisClient.on('error', (err: Error) => console.error('Redis Client Error', err));
|
||||
await redisClient.connect();
|
||||
console.error(`Connected to Redis successfully at ${REDIS_URL}`);
|
||||
// Set up Redis event handlers
|
||||
redisClient.on('error', (err: Error) => {
|
||||
console.error('Redis Client Error:', err);
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
console.error(`Connected to Redis at ${REDIS_URL}`);
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
console.error('Attempting to reconnect to Redis...');
|
||||
});
|
||||
|
||||
redisClient.on('end', () => {
|
||||
console.error('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
await redisClient.connect();
|
||||
|
||||
// Set up MCP server
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Redis MCP Server running on stdio");
|
||||
} catch (error) {
|
||||
console.error("Error during startup:", error);
|
||||
await redisClient.quit();
|
||||
process.exit(1);
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
async function cleanup() {
|
||||
try {
|
||||
await redisClient.quit();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
redisClient.quit().finally(() => process.exit(1));
|
||||
});
|
||||
cleanup();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user