Cursor MCP Installer
by matthewdcage
Verified
#!/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 * as os from "os";
import * as fs from "fs";
import * as path from "path";
import { spawnPromise } from "spawn-rx";
const server = new Server(
{
name: "cursor-mcp-installer-free",
version: "0.1.1",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "install_repo_mcp_server",
description: "Install an MCP server via npx or uvx",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The package name of the MCP server",
},
args: {
type: "array",
items: { type: "string" },
description: "The arguments to pass along",
},
env: {
type: "array",
items: { type: "string" },
description: "The environment variables to set, delimited by =",
},
},
required: ["name"],
},
},
{
name: "install_local_mcp_server",
description:
"Install an MCP server whose code is cloned locally on your computer",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description:
"The path to the MCP server code cloned on your computer",
},
args: {
type: "array",
items: { type: "string" },
description: "The arguments to pass along",
},
env: {
type: "array",
items: { type: "string" },
description: "The environment variables to set, delimited by =",
},
},
required: ["path"],
},
},
{
name: "add_to_cursor_config",
description:
"Add any MCP server to Cursor's configuration",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Display name for the MCP server in Cursor",
},
command: {
type: "string",
description: "Command to execute (e.g., node, npx, python)",
},
args: {
type: "array",
items: { type: "string" },
description: "Arguments to pass to the command",
},
path: {
type: "string",
description: "Path to the MCP server on disk (optional, used instead of command+args)",
},
env: {
type: "array",
items: { type: "string" },
description: "Environment variables to set, delimited by =",
},
},
required: ["name"],
},
},
],
};
});
async function hasNodeJs() {
try {
await spawnPromise("node", ["--version"]);
return true;
} catch (e) {
return false;
}
}
async function hasUvx() {
try {
await spawnPromise("uvx", ["--version"]);
return true;
} catch (e) {
return false;
}
}
async function isNpmPackage(name: string) {
try {
await spawnPromise("npm", ["view", name, "version"]);
return true;
} catch (e) {
return false;
}
}
function installToCursor(
name: string,
cmd: string,
args: string[],
env?: string[]
) {
const configPath = path.join(os.homedir(), ".cursor", "mcp.json");
let config: any;
try {
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
} catch (e) {
config = { mcpServers: {} };
}
const envObj = (env ?? []).reduce((acc, val) => {
const [key, value] = val.split("=");
acc[key] = value;
return acc;
}, {} as Record<string, string>);
const newServer = {
command: cmd,
type: "stdio",
args: args,
...(env ? { env: envObj } : {}),
};
config.mcpServers = config.mcpServers || {};
config.mcpServers[name] = newServer;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
function installRepoWithArgsToCursor(
name: string,
npmIfTrueElseUvx: boolean,
args?: string[],
env?: string[]
) {
// If the name is in a scoped package, we need to remove the scope
const serverName = /^@.*\//i.test(name) ? name.split("/")[1] : name;
// For Cursor, create a friendly display name
const formattedName = serverName
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
// Check if we're using a package that requires special handling
if (name === 'mcp-openapi-schema' || name.includes('openapi-schema')) {
// For OpenAPI schema servers, we need to pass the schema file as an argument to index.mjs
const hasSchemaFile = args && args.length > 0 &&
(args[0].endsWith('.yaml') || args[0].endsWith('.json') || args[0].endsWith('.yml'));
if (hasSchemaFile) {
// Special configuration for OpenAPI schema servers
// First try to get the installed package path
try {
const packagePath = path.dirname(require.resolve(`${name}/package.json`));
const indexPath = path.join(packagePath, 'index.mjs');
if (fs.existsSync(indexPath)) {
installToCursor(
formattedName,
'node',
[indexPath, ...(args ?? [])],
env
);
return;
}
} catch (error) {
console.warn(`Couldn't resolve ${name} package path, falling back to npx`);
}
}
}
// Check if this is a Python package
if (!npmIfTrueElseUvx || name.includes('x-mcp') || name.includes('python') || name.endsWith('.py')) {
// For Python MCP servers, we should use python -m module_name pattern instead of uvx
// This helps ensure proper module paths and environment
const moduleName = name.replace(/-/g, '_').replace(/\.git$/, '');
// Extract module name from common patterns like username/repo-name.git
const cleanModuleName = moduleName.includes('/') ?
moduleName.split('/').pop()!.replace(/\.git$/, '') :
moduleName;
// For X Twitter MCP specifically
if (name.includes('x-mcp')) {
installToCursor(
'X Twitter Tools',
'python3',
['-m', `${cleanModuleName.replace(/-/g, '_')}.server`],
env
);
return;
}
// For other Python-based MCP servers
installToCursor(
formattedName,
'python3',
['-m', cleanModuleName],
env
);
return;
}
// Default case - use npx/uvx
installToCursor(
formattedName,
npmIfTrueElseUvx ? "npx" : "uvx",
["-y", name, ...(args ?? [])],
env
);
}
async function attemptNodeInstall(
directory: string
): Promise<Record<string, string>> {
await spawnPromise("npm", ["install"], { cwd: directory });
// Run down package.json looking for bins
const pkg = JSON.parse(
fs.readFileSync(path.join(directory, "package.json"), "utf-8")
);
if (pkg.bin) {
return Object.keys(pkg.bin).reduce((acc, key) => {
acc[key] = path.resolve(directory, pkg.bin[key]);
return acc;
}, {} as Record<string, string>);
}
if (pkg.main) {
return { [pkg.name]: path.resolve(directory, pkg.main) };
}
return {};
}
async function addToCursorConfig(
name: string,
command?: string,
args?: string[],
path?: string,
env?: string[]
) {
// Determine if we're using command+args or a direct path
let cmd = command || "node";
let cmdArgs = args || [];
// If path is provided, use it directly
if (path) {
if (path.endsWith(".js") || path.endsWith(".mjs")) {
cmd = "node";
cmdArgs = [path];
} else if (path.endsWith(".py")) {
cmd = "python";
cmdArgs = [path];
} else {
// Assume it's an executable
cmd = path;
cmdArgs = [];
}
}
// Format the name nicely for display
const formattedName = name
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
// Install to Cursor config
installToCursor(formattedName, cmd, cmdArgs, env);
return {
content: [
{
type: "text",
text: `Added ${formattedName} to Cursor's MCP configuration. Please restart Cursor to apply the changes.`,
},
],
};
}
async function installLocalMcpServer(
dirPath: string,
args?: string[],
env?: string[]
) {
if (!fs.existsSync(dirPath)) {
return {
content: [
{
type: "text",
text: `Path ${dirPath} does not exist locally!`,
},
],
isError: true,
};
}
if (fs.existsSync(path.join(dirPath, "package.json"))) {
const servers = await attemptNodeInstall(dirPath);
Object.keys(servers).forEach((name) => {
// Install to Cursor
const formattedName = name
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
installToCursor(
formattedName,
"node",
[servers[name], ...(args ?? [])],
env
);
});
return {
content: [
{
type: "text",
text: `Installed the following servers to Cursor: ${Object.keys(
servers
).join(", ")}. Please restart Cursor to apply the changes.`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Can't figure out how to install ${dirPath}`,
},
],
isError: true,
};
}
async function installRepoMcpServer(
name: string,
args?: string[],
env?: string[]
) {
if (!(await hasNodeJs())) {
return {
content: [
{
type: "text",
text: `Node.js is not installed, please install it!`,
},
],
isError: true,
};
}
// Check if this is a git repository URL
const isGitRepo = name.endsWith('.git') || name.includes('github.com') || name.includes('gitlab.com');
if (isGitRepo) {
// For Git repos, we need to clone and examine the structure
const repoName = name.split('/').pop()?.replace('.git', '') || 'mcp-repo';
const tempDir = path.join(os.tmpdir(), `mcp-${repoName}-${Date.now()}`);
try {
fs.mkdirSync(tempDir, { recursive: true });
await spawnPromise('git', ['clone', name, tempDir]);
// Check if the repo has an index.mjs/index.js file at the root
const hasIndexMjs = fs.existsSync(path.join(tempDir, 'index.mjs'));
const hasIndexJs = fs.existsSync(path.join(tempDir, 'index.js'));
if (hasIndexMjs || hasIndexJs) {
const indexFile = hasIndexMjs ? 'index.mjs' : 'index.js';
// Check if package.json exists before parsing
if (fs.existsSync(path.join(tempDir, 'package.json'))) {
// Install dependencies
await spawnPromise('npm', ['install'], { cwd: tempDir });
}
// Create a friendly name from the repo name
const friendlyName = repoName
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
// Install to Cursor, properly handling the index file
const mainFilePath = path.join(tempDir, indexFile);
installToCursor(friendlyName, 'node', [mainFilePath, ...(args || [])], env);
return {
content: [
{
type: "text",
text: `Installed ${friendlyName} MCP server from Git repository to Cursor successfully! Please restart Cursor to apply the changes.`,
},
],
};
}
} catch (error: any) {
console.error(`Error cloning repository: ${error.message || 'Unknown error'}`);
return {
content: [
{
type: "text",
text: `Error installing from Git repository: ${error.message || 'Unknown error'}`,
},
],
isError: true,
};
}
}
if (await isNpmPackage(name)) {
// Install to Cursor
installRepoWithArgsToCursor(name, true, args, env);
return {
content: [
{
type: "text",
text: "Installed MCP server via npx to Cursor successfully! Please restart Cursor to apply the changes.",
},
],
};
}
if (!(await hasUvx())) {
return {
content: [
{
type: "text",
text: `Python uv is not installed, please install it! Go to https://docs.astral.sh/uv for installation instructions.`,
},
],
isError: true,
};
}
// Install to Cursor
installRepoWithArgsToCursor(name, false, args, env);
return {
content: [
{
type: "text",
text: "Installed MCP server via uvx to Cursor successfully! Please restart Cursor to apply the changes.",
},
],
};
}
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (request.params.name === "install_repo_mcp_server") {
const { name, args, env } = request.params.arguments as {
name: string;
args?: string[];
env?: string[];
};
return await installRepoMcpServer(name, args, env);
}
if (request.params.name === "install_local_mcp_server") {
const dirPath = request.params.arguments!.path as string;
const { args, env } = request.params.arguments as {
args?: string[];
env?: string[];
};
return await installLocalMcpServer(dirPath, args, env);
}
if (request.params.name === "add_to_cursor_config") {
const { name, command, args, path, env } = request.params.arguments as {
name: string;
command?: string;
args?: string[];
path?: string;
env?: string[];
};
return await addToCursorConfig(name, command, args, path, env);
}
throw new Error(`Unknown tool: ${request.params.name}`);
} catch (err) {
return {
content: [
{
type: "text",
text: `Error setting up package: ${err}`,
},
],
isError: true,
};
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);