mcpConfig.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import fsPromises from 'fs/promises';
  2. import os from 'os';
  3. import path from 'path';
  4. const DEFAULT_MCP_CONFIG = {
  5. mcpServers: {},
  6. };
  7. function pilotHome() {
  8. return process.env.PILOT_HOME || path.join(os.homedir(), '.pilotdeck');
  9. }
  10. export function getGlobalMcpConfigPath() {
  11. return path.join(pilotHome(), 'mcp.json');
  12. }
  13. export function getProjectMcpConfigPath(projectPath) {
  14. return path.join(projectPath || process.cwd(), '.pilotdeck', 'mcp.json');
  15. }
  16. export function normalizeMcpConfig(input) {
  17. if (!input || typeof input !== 'object' || Array.isArray(input)) {
  18. throw new Error('MCP config root must be an object.');
  19. }
  20. if (input.mcpServers === undefined) {
  21. return { ...input, mcpServers: {} };
  22. }
  23. if (!input.mcpServers || typeof input.mcpServers !== 'object' || Array.isArray(input.mcpServers)) {
  24. throw new Error('mcpServers must be an object.');
  25. }
  26. for (const [name, server] of Object.entries(input.mcpServers)) {
  27. if (!name || !server || typeof server !== 'object' || Array.isArray(server)) {
  28. throw new Error(`Server "${name}" must be an object.`);
  29. }
  30. if (typeof server.command !== 'string' && typeof server.url !== 'string' && typeof server.httpUrl !== 'string') {
  31. throw new Error(`Server "${name}" must define command, url, or httpUrl.`);
  32. }
  33. if (server.args !== undefined && (!Array.isArray(server.args) || !server.args.every((arg) => typeof arg === 'string'))) {
  34. throw new Error(`Server "${name}" args must be an array of strings.`);
  35. }
  36. if (server.env !== undefined && !isStringRecord(server.env)) {
  37. throw new Error(`Server "${name}" env must be an object with string values.`);
  38. }
  39. if (server.headers !== undefined && !isStringRecord(server.headers)) {
  40. throw new Error(`Server "${name}" headers must be an object with string values.`);
  41. }
  42. }
  43. return input;
  44. }
  45. export async function readMcpConfigFile(scope, projectPath) {
  46. const filePath = scope === 'project' ? getProjectMcpConfigPath(projectPath) : getGlobalMcpConfigPath();
  47. try {
  48. const raw = await fsPromises.readFile(filePath, 'utf8');
  49. const parsed = normalizeMcpConfig(JSON.parse(raw));
  50. return {
  51. exists: true,
  52. path: filePath,
  53. raw: JSON.stringify(parsed, null, 2),
  54. config: parsed,
  55. };
  56. } catch (error) {
  57. if (error?.code === 'ENOENT') {
  58. return {
  59. exists: false,
  60. path: filePath,
  61. raw: JSON.stringify(DEFAULT_MCP_CONFIG, null, 2),
  62. config: DEFAULT_MCP_CONFIG,
  63. };
  64. }
  65. throw error;
  66. }
  67. }
  68. export async function writeMcpConfigFile(scope, raw, projectPath) {
  69. const parsed = normalizeMcpConfig(JSON.parse(raw));
  70. const filePath = scope === 'project' ? getProjectMcpConfigPath(projectPath) : getGlobalMcpConfigPath();
  71. await fsPromises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
  72. await fsPromises.writeFile(filePath, JSON.stringify(parsed, null, 2) + '\n', { mode: 0o600 });
  73. return readMcpConfigFile(scope, projectPath);
  74. }
  75. export async function listMcpConfigFiles(projectPath) {
  76. const [globalConfig, projectConfig] = await Promise.all([
  77. readMcpConfigFile('global', projectPath),
  78. readMcpConfigFile('project', projectPath),
  79. ]);
  80. return {
  81. global: globalConfig,
  82. project: projectConfig,
  83. };
  84. }
  85. function isStringRecord(value) {
  86. return value && typeof value === 'object' && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === 'string');
  87. }