Config tweaks for docker
- Remove `$` interpolation for env - Allow puppeteer to work headless in docker, headful with npx
This commit is contained in:
parent
308b71c698
commit
c64f8de15f
12 changed files with 72 additions and 59 deletions
|
|
@ -34,7 +34,7 @@ Add this to your `claude_desktop_config.json`:
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aws-kb-retrieval": {
|
"aws-kb-retrieval": {
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"args": [ "run", "-i", "--rm", "-e", "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID", "-e", "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY", "-e", "AWS_REGION=$AWS_REGION", "ai/mcp-aws-kb-retrieval-server" ],
|
"args": [ "run", "-i", "--rm", "-e", "AWS_ACCESS_KEY_ID", "-e", "AWS_SECRET_ACCESS_KEY", "-e", "AWS_REGION", "ai/mcp-aws-kb-retrieval-server" ],
|
||||||
"env": {
|
"env": {
|
||||||
"AWS_ACCESS_KEY_ID": "YOUR_ACCESS_KEY_HERE",
|
"AWS_ACCESS_KEY_ID": "YOUR_ACCESS_KEY_HERE",
|
||||||
"AWS_SECRET_ACCESS_KEY": "YOUR_SECRET_ACCESS_KEY_HERE",
|
"AWS_SECRET_ACCESS_KEY": "YOUR_SECRET_ACCESS_KEY_HERE",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ Add this to your `claude_desktop_config.json`:
|
||||||
"-i",
|
"-i",
|
||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"BRAVE_API_KEY=$BRAVE_API_KEY",
|
"BRAVE_API_KEY",
|
||||||
"ai/mcp-brave-search"
|
"ai/mcp-brave-search"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Add to Claude Desktop config:
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"everart": {
|
"everart": {
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"args": ["run", "-i", "--rm", "-e", "EVERART_API_KEY=$EVERART_API_KEY", "ai/mcp-everart"],
|
"args": ["run", "-i", "--rm", "-e", "EVERART_API_KEY", "ai/mcp-everart"],
|
||||||
"env": {
|
"env": {
|
||||||
"EVERART_API_KEY": "your_key_here"
|
"EVERART_API_KEY": "your_key_here"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,6 @@ ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN npm ci --ignore-scripts --omit-dev
|
RUN npm ci --ignore-scripts --omit-dev
|
||||||
|
|
||||||
ENTRYPOINT ["node", "dist/index.js"]
|
WORKDIR /projects
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "/app/dist/index.js"]
|
||||||
|
|
@ -109,6 +109,7 @@ Add this to your `claude_desktop_config.json`:
|
||||||
Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
|
Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
Note: all directories must be mounted to `/projects` by default.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -122,7 +123,6 @@ Note: you can provide sandboxed directories to the server by mounting them to `/
|
||||||
"--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop",
|
"--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop",
|
||||||
"--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro",
|
"--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro",
|
||||||
"--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt",
|
"--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt",
|
||||||
"--env", "DOCKER_ROOT_WORKSPACE=/projects",
|
|
||||||
"ai/mcp-filesystem"
|
"ai/mcp-filesystem"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,15 @@ const args = process.argv.slice(2);
|
||||||
|
|
||||||
let allowedDirectories: string[] = [];
|
let allowedDirectories: string[] = [];
|
||||||
|
|
||||||
if (process.env.DOCKER_ROOT_WORKSPACE) {
|
let hasProjectsDirectory = false;
|
||||||
allowedDirectories = await fs.readdir(process.env.DOCKER_ROOT_WORKSPACE);
|
|
||||||
|
// If the projects directory has contents, use it as the root workspace
|
||||||
|
const projectsContents = await fs.readdir('/projects');
|
||||||
|
if (projectsContents.length > 0) {
|
||||||
|
hasProjectsDirectory = true;
|
||||||
|
allowedDirectories = projectsContents.map(dir =>
|
||||||
|
normalizePath(path.resolve(expandHome(dir)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
|
|
@ -51,7 +58,7 @@ function expandHome(filepath: string): string {
|
||||||
await Promise.all(allowedDirectories.map(async (dir) => {
|
await Promise.all(allowedDirectories.map(async (dir) => {
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(dir);
|
const stats = await fs.stat(dir);
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory() && !hasProjectsDirectory) {
|
||||||
console.error(`Error: ${dir} is not a directory`);
|
console.error(`Error: ${dir} is not a directory`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +269,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
|
||||||
|
|
||||||
async function applyFileEdits(
|
async function applyFileEdits(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
edits: Array<{oldText: string, newText: string}>,
|
edits: Array<{ oldText: string, newText: string }>,
|
||||||
dryRun = false
|
dryRun = false
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Read file content and normalize line endings
|
// Read file content and normalize line endings
|
||||||
|
|
@ -398,10 +405,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
{
|
{
|
||||||
name: "directory_tree",
|
name: "directory_tree",
|
||||||
description:
|
description:
|
||||||
"Get a recursive tree view of files and directories as a JSON structure. " +
|
"Get a recursive tree view of files and directories as a JSON structure. " +
|
||||||
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||||
"Files have no children array, while directories always have a children array (which may be empty). " +
|
"Files have no children array, while directories always have a children array (which may be empty). " +
|
||||||
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
||||||
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -538,49 +545,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "directory_tree": {
|
case "directory_tree": {
|
||||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||||
}
|
|
||||||
|
|
||||||
interface TreeEntry {
|
|
||||||
name: string;
|
|
||||||
type: 'file' | 'directory';
|
|
||||||
children?: TreeEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
|
|
||||||
const validPath = await validatePath(currentPath);
|
|
||||||
const entries = await fs.readdir(validPath, {withFileTypes: true});
|
|
||||||
const result: TreeEntry[] = [];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryData: TreeEntry = {
|
|
||||||
name: entry.name,
|
|
||||||
type: entry.isDirectory() ? 'directory' : 'file'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const subPath = path.join(currentPath, entry.name);
|
|
||||||
entryData.children = await buildTree(subPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(entryData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const treeData = await buildTree(parsed.data.path);
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(treeData, null, 2)
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TreeEntry {
|
||||||
|
name: string;
|
||||||
|
type: 'file' | 'directory';
|
||||||
|
children?: TreeEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
|
||||||
|
const validPath = await validatePath(currentPath);
|
||||||
|
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||||
|
const result: TreeEntry[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryData: TreeEntry = {
|
||||||
|
name: entry.name,
|
||||||
|
type: entry.isDirectory() ? 'directory' : 'file'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const subPath = path.join(currentPath, entry.name);
|
||||||
|
entryData.children = await buildTree(subPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(entryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = await buildTree(parsed.data.path);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(treeData, null, 2)
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "move_file": {
|
case "move_file": {
|
||||||
const parsed = MoveFileArgsSchema.safeParse(args);
|
const parsed = MoveFileArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|
@ -614,9 +621,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
const validPath = await validatePath(parsed.data.path);
|
const validPath = await validatePath(parsed.data.path);
|
||||||
const info = await getFileStats(validPath);
|
const info = await getFileStats(validPath);
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: Object.entries(info)
|
content: [{
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
type: "text", text: Object.entries(info)
|
||||||
.join("\n") }],
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join("\n")
|
||||||
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ To use this with Claude Desktop, add the following to your `claude_desktop_confi
|
||||||
"-i",
|
"-i",
|
||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PERSONAL_ACCESS_TOKEN",
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
"ai/mcp-github"
|
"ai/mcp-github"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -118,9 +118,9 @@ Add the following to your `claude_desktop_config.json`:
|
||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run",
|
||||||
"-e",
|
"-e",
|
||||||
"GITLAB_PERSONAL_ACCESS_TOKEN=$GITLAB_PERSONAL_ACCESS_TOKEN",
|
"GITLAB_PERSONAL_ACCESS_TOKEN",
|
||||||
"-e",
|
"-e",
|
||||||
"GITLAB_API_URL=$GITLAB_API_URL",
|
"GITLAB_API_URL",
|
||||||
"ai/mcp-gitlab"
|
"ai/mcp-gitlab"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ Add the following to your `claude_desktop_config.json`:
|
||||||
"-i",
|
"-i",
|
||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY",
|
"GOOGLE_MAPS_API_KEY",
|
||||||
"ai/mcp-google-maps"
|
"ai/mcp-google-maps"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ Here's the Claude Desktop configuration to use the Puppeter server:
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"puppeteer": {
|
"puppeteer": {
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"args": ["run", "-i", "--rm", "--init", "ai/mcp-puppeteer"]
|
"args": ["run", "-i", "--rm", "--init", "-e", "DOCKER_CONTAINER=true", "ai/mcp-puppeteer"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ const screenshots = new Map<string, string>();
|
||||||
|
|
||||||
async function ensureBrowser() {
|
async function ensureBrowser() {
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
|
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);
|
||||||
const pages = await browser.pages();
|
const pages = await browser.pages();
|
||||||
page = pages[0];
|
page = pages[0];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,9 @@ Add the following to your `claude_desktop_config.json`:
|
||||||
"-i",
|
"-i",
|
||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN",
|
"SLACK_BOT_TOKEN",
|
||||||
"-e",
|
"-e",
|
||||||
"SLACK_TEAM_ID=$SLACK_TEAM_ID",
|
"SLACK_TEAM_ID",
|
||||||
"ai/mcp-slack"
|
"ai/mcp-slack"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue