mcpsmgr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +1631 -0
  5. package/dist/index.js.map +1 -0
  6. package/docs/README_zh-CN.md +99 -0
  7. package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/.openspec.yaml +2 -0
  8. package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/design.md +41 -0
  9. package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/proposal.md +28 -0
  10. package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/specs/project-operations/spec.md +53 -0
  11. package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/tasks.md +9 -0
  12. package/openspec/changes/archive/2026-03-12-fix-init-server-detection/.openspec.yaml +2 -0
  13. package/openspec/changes/archive/2026-03-12-fix-init-server-detection/design.md +40 -0
  14. package/openspec/changes/archive/2026-03-12-fix-init-server-detection/proposal.md +25 -0
  15. package/openspec/changes/archive/2026-03-12-fix-init-server-detection/specs/project-operations/spec.md +25 -0
  16. package/openspec/changes/archive/2026-03-12-fix-init-server-detection/tasks.md +10 -0
  17. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/.openspec.yaml +2 -0
  18. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/design.md +32 -0
  19. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/proposal.md +25 -0
  20. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/specs/project-operations/spec.md +30 -0
  21. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/specs/server-management/spec.md +15 -0
  22. package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/tasks.md +17 -0
  23. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/.openspec.yaml +2 -0
  24. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/design.md +104 -0
  25. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/proposal.md +34 -0
  26. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/agent-adapters/spec.md +110 -0
  27. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/central-storage/spec.md +38 -0
  28. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/glm-integration/spec.md +66 -0
  29. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/project-operations/spec.md +76 -0
  30. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/server-management/spec.md +75 -0
  31. package/openspec/changes/archive/2026-03-12-mcps-manager-cli/tasks.md +60 -0
  32. package/openspec/config.yaml +20 -0
  33. package/openspec/specs/agent-adapters/spec.md +148 -0
  34. package/openspec/specs/central-storage/spec.md +42 -0
  35. package/openspec/specs/glm-integration/spec.md +70 -0
  36. package/openspec/specs/project-operations/spec.md +138 -0
  37. package/openspec/specs/server-management/spec.md +93 -0
  38. package/package.json +33 -0
  39. package/src/__tests__/integration.test.ts +200 -0
  40. package/src/adapters/__tests__/adapters.test.ts +274 -0
  41. package/src/adapters/antigravity.ts +114 -0
  42. package/src/adapters/claude-code.ts +114 -0
  43. package/src/adapters/codex-cli.ts +135 -0
  44. package/src/adapters/env-args.ts +51 -0
  45. package/src/adapters/gemini-cli.ts +110 -0
  46. package/src/adapters/index.ts +32 -0
  47. package/src/adapters/json-file.ts +24 -0
  48. package/src/adapters/opencode.ts +114 -0
  49. package/src/commands/add.ts +68 -0
  50. package/src/commands/init.ts +136 -0
  51. package/src/commands/list.ts +77 -0
  52. package/src/commands/remove.ts +61 -0
  53. package/src/commands/server-add.ts +211 -0
  54. package/src/commands/server-list.ts +24 -0
  55. package/src/commands/server-remove.ts +12 -0
  56. package/src/commands/setup.ts +71 -0
  57. package/src/commands/sync.ts +98 -0
  58. package/src/index.ts +100 -0
  59. package/src/services/glm-client.ts +190 -0
  60. package/src/services/system-prompt.ts +61 -0
  61. package/src/services/web-reader.ts +130 -0
  62. package/src/types.ts +59 -0
  63. package/src/utils/config.ts +22 -0
  64. package/src/utils/paths.ts +11 -0
  65. package/src/utils/prompt.ts +3 -0
  66. package/src/utils/resolve-config.ts +13 -0
  67. package/src/utils/server-store.ts +56 -0
  68. package/tsconfig.json +17 -0
  69. package/tsup.config.ts +13 -0
  70. package/vitest.config.ts +8 -0
@@ -0,0 +1,190 @@
1
+ import type { GlobalConfig } from "../types.js";
2
+ import { fetchWebContent } from "./web-reader.js";
3
+ import { ANALYSIS_SYSTEM_PROMPT } from "./system-prompt.js";
4
+
5
+ interface GlmMessage {
6
+ readonly role: "system" | "user" | "assistant" | "tool";
7
+ readonly content: string | null;
8
+ readonly tool_calls?: readonly GlmToolCall[];
9
+ readonly tool_call_id?: string;
10
+ }
11
+
12
+ interface GlmToolCall {
13
+ readonly id: string;
14
+ readonly type: "function";
15
+ readonly function: {
16
+ readonly name: string;
17
+ readonly arguments: string;
18
+ };
19
+ }
20
+
21
+ interface GlmResponse {
22
+ readonly choices: readonly {
23
+ readonly message: GlmMessage;
24
+ readonly finish_reason: string;
25
+ }[];
26
+ }
27
+
28
+ const WEB_READER_TOOL = {
29
+ type: "function" as const,
30
+ function: {
31
+ name: "webReader",
32
+ description: "Fetch and read the content of a web page given its URL",
33
+ parameters: {
34
+ type: "object",
35
+ properties: {
36
+ url: {
37
+ type: "string",
38
+ description: "The URL of the web page to read",
39
+ },
40
+ },
41
+ required: ["url"],
42
+ },
43
+ },
44
+ };
45
+
46
+ export interface AnalysisResult {
47
+ readonly name: string;
48
+ readonly default: {
49
+ readonly transport: "stdio" | "http";
50
+ readonly command?: string;
51
+ readonly args?: readonly string[];
52
+ readonly env?: Readonly<Record<string, string>>;
53
+ readonly url?: string;
54
+ readonly headers?: Readonly<Record<string, string>>;
55
+ };
56
+ readonly overrides: Readonly<Record<string, unknown>>;
57
+ readonly requiredEnvVars: readonly string[];
58
+ }
59
+
60
+ async function callGlm(
61
+ config: GlobalConfig,
62
+ messages: GlmMessage[],
63
+ ): Promise<GlmResponse> {
64
+ const response = await fetch(config.glm.endpoint, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ Authorization: `Bearer ${config.glm.apiKey}`,
69
+ },
70
+ body: JSON.stringify({
71
+ model: "glm-5",
72
+ messages,
73
+ tools: [WEB_READER_TOOL],
74
+ tool_choice: "auto",
75
+ }),
76
+ });
77
+
78
+ if (!response.ok) {
79
+ throw new Error(
80
+ `GLM5 API request failed: ${response.status} ${response.statusText}`,
81
+ );
82
+ }
83
+
84
+ return (await response.json()) as GlmResponse;
85
+ }
86
+
87
+ export async function analyzeWithGlm(
88
+ config: GlobalConfig,
89
+ userMessage: string,
90
+ ): Promise<AnalysisResult> {
91
+ const messages: GlmMessage[] = [
92
+ { role: "system", content: ANALYSIS_SYSTEM_PROMPT },
93
+ { role: "user", content: userMessage },
94
+ ];
95
+
96
+ const MAX_ROUNDS = 10;
97
+ for (let round = 0; round < MAX_ROUNDS; round++) {
98
+ const response = await callGlm(config, messages);
99
+ const choice = response.choices.at(0);
100
+ if (!choice) {
101
+ throw new Error("GLM5 returned empty response");
102
+ }
103
+
104
+ const assistantMessage = choice.message;
105
+ messages.push({
106
+ role: "assistant",
107
+ content: assistantMessage.content,
108
+ tool_calls: assistantMessage.tool_calls,
109
+ });
110
+
111
+ if (
112
+ !assistantMessage.tool_calls ||
113
+ assistantMessage.tool_calls.length === 0
114
+ ) {
115
+ return parseAnalysisResult(assistantMessage.content ?? "");
116
+ }
117
+
118
+ for (const toolCall of assistantMessage.tool_calls) {
119
+ if (toolCall.function.name === "webReader") {
120
+ const args = JSON.parse(toolCall.function.arguments) as {
121
+ url: string;
122
+ };
123
+ let toolResult: string;
124
+ try {
125
+ toolResult = await fetchWebContent(config, args.url);
126
+ } catch (error) {
127
+ toolResult = `Error fetching URL: ${error instanceof Error ? error.message : String(error)}`;
128
+ }
129
+ messages.push({
130
+ role: "tool",
131
+ content: toolResult,
132
+ tool_call_id: toolCall.id,
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ throw new Error("GLM5 analysis exceeded maximum rounds");
139
+ }
140
+
141
+ function parseAnalysisResult(content: string): AnalysisResult {
142
+ const cleaned = content
143
+ .replace(/```json\s*/g, "")
144
+ .replace(/```\s*/g, "")
145
+ .trim();
146
+
147
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
148
+ if (!jsonMatch) {
149
+ throw new Error(`Cannot extract JSON from GLM5 response: ${cleaned.slice(0, 200)}`);
150
+ }
151
+
152
+ const result = JSON.parse(jsonMatch[0]) as AnalysisResult;
153
+ if (!result.name || !result.default) {
154
+ throw new Error("Invalid analysis result: missing name or default config");
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ export function buildUserMessage(input: string): string {
161
+ if (isGitHubRepo(input)) {
162
+ const url = `https://github.com/${input}`;
163
+ return `Please analyze the MCP server at ${url}. Start by reading the README at ${url}/blob/main/README.md`;
164
+ }
165
+ return `Please analyze the MCP server documentation at: ${input}`;
166
+ }
167
+
168
+ export function isGitHubRepo(input: string): boolean {
169
+ if (input.startsWith("http") || input.startsWith("@")) {
170
+ return false;
171
+ }
172
+ const parts = input.split("/");
173
+ return parts.length === 2 && parts.every((p) => p.length > 0);
174
+ }
175
+
176
+ export function isValidInput(
177
+ input: string,
178
+ ): { valid: true } | { valid: false; reason: string } {
179
+ if (input.startsWith("http://") || input.startsWith("https://")) {
180
+ return { valid: true };
181
+ }
182
+ if (isGitHubRepo(input)) {
183
+ return { valid: true };
184
+ }
185
+ return {
186
+ valid: false,
187
+ reason:
188
+ 'Invalid input format. Provide a URL (https://...) or GitHub owner/repo (e.g., "anthropics/mcp-brave-search")',
189
+ };
190
+ }
@@ -0,0 +1,61 @@
1
+ export const ANALYSIS_SYSTEM_PROMPT = `You are an MCP (Model Context Protocol) server configuration analyst. Your task is to analyze documentation for an MCP server and extract configuration details for 5 different coding agents.
2
+
3
+ The 5 agents and their configuration differences:
4
+
5
+ 1. **Claude Code** (.mcp.json)
6
+ - Format: { "type": "stdio"|"http", "command": "...", "args": [...] }
7
+ - HTTP: { "type": "http", "url": "...", "headers": {...} }
8
+ - IMPORTANT: Do NOT use "env" field. Environment variables will be handled separately.
9
+
10
+ 2. **Codex CLI** (.codex/config.toml)
11
+ - TOML format: command = "...", args = [...]
12
+ - Same key names as Claude Code but in TOML
13
+ - IMPORTANT: Do NOT use "env" field.
14
+
15
+ 3. **Gemini CLI** (.gemini/settings.json)
16
+ - Format: { "command": "...", "args": [...] }
17
+ - No "type" field needed
18
+ - IMPORTANT: Do NOT use "env" field.
19
+
20
+ 4. **OpenCode** (opencode.json)
21
+ - Format: { "type": "local"|"remote", "command": ["cmd", "arg1", ...] }
22
+ - command is an ARRAY including the command itself
23
+ - type is "local" for stdio, "remote" for http
24
+ - IMPORTANT: Do NOT use "environment" field.
25
+
26
+ 5. **Antigravity** (~/.gemini/antigravity/mcp_config.json)
27
+ - Format: { "command": "...", "args": [...] }
28
+ - HTTP: { "serverUrl": "...", "headers": {...} } (note: "serverUrl" not "url")
29
+ - IMPORTANT: Do NOT use "env" field.
30
+
31
+ You have access to a webReader tool to fetch web page content. Use it to read the documentation URL provided.
32
+
33
+ After analyzing the documentation, return a JSON object with this exact structure:
34
+ \`\`\`json
35
+ {
36
+ "name": "server-name",
37
+ "default": {
38
+ "transport": "stdio",
39
+ "command": "npx",
40
+ "args": ["-y", "@scope/package"],
41
+ },
42
+ "overrides": {
43
+ "opencode": {
44
+ "transport": "stdio",
45
+ "command": "npx",
46
+ "args": ["-y", "@scope/package"],
47
+ "env": {}
48
+ }
49
+ },
50
+ "requiredEnvVars": ["API_KEY"]
51
+ }
52
+ \`\`\`
53
+
54
+ Rules:
55
+ - "name" should be a kebab-case identifier for the server
56
+ - "default" should be the most common configuration (usually works for Claude Code, Codex CLI, Gemini CLI)
57
+ - Only add "overrides" for agents that need DIFFERENT configuration from the default
58
+ - OpenCode usually needs an override because its command format differs (array vs string+args)
59
+ - "requiredEnvVars" lists environment variable names the user needs to provide values for
60
+ - Transport is either "stdio" or "http"
61
+ - Return ONLY the JSON object, no markdown fences, no explanation`;
@@ -0,0 +1,130 @@
1
+ import type { GlobalConfig } from "../types.js";
2
+
3
+ interface McpResponse {
4
+ readonly jsonrpc: string;
5
+ readonly id: number;
6
+ readonly result?: {
7
+ readonly content?: readonly { readonly type: string; readonly text: string }[];
8
+ readonly isError?: boolean;
9
+ };
10
+ readonly error?: { readonly code: number; readonly message: string };
11
+ }
12
+
13
+ function parseSseResponse(raw: string): McpResponse {
14
+ const lines = raw.split("\n");
15
+ for (const line of lines) {
16
+ if (line.startsWith("data:")) {
17
+ return JSON.parse(line.slice(5)) as McpResponse;
18
+ }
19
+ }
20
+ return JSON.parse(raw) as McpResponse;
21
+ }
22
+
23
+ async function mcpInitialize(
24
+ endpoint: string,
25
+ apiKey: string,
26
+ ): Promise<string> {
27
+ const response = await fetch(endpoint, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ Accept: "application/json, text/event-stream",
32
+ Authorization: `Bearer ${apiKey}`,
33
+ },
34
+ body: JSON.stringify({
35
+ jsonrpc: "2.0",
36
+ id: 1,
37
+ method: "initialize",
38
+ params: {
39
+ protocolVersion: "2025-03-26",
40
+ capabilities: {},
41
+ clientInfo: { name: "mcpsmgr", version: "0.1.0" },
42
+ },
43
+ }),
44
+ });
45
+
46
+ if (!response.ok) {
47
+ throw new Error(
48
+ `MCP initialize failed: ${response.status} ${response.statusText}`,
49
+ );
50
+ }
51
+
52
+ const sessionId = response.headers.get("mcp-session-id");
53
+ if (!sessionId) {
54
+ throw new Error("MCP server did not return session ID");
55
+ }
56
+
57
+ return sessionId;
58
+ }
59
+
60
+ async function mcpToolCall(
61
+ endpoint: string,
62
+ apiKey: string,
63
+ sessionId: string,
64
+ toolName: string,
65
+ args: Record<string, unknown>,
66
+ ): Promise<string> {
67
+ const response = await fetch(endpoint, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Accept: "application/json, text/event-stream",
72
+ Authorization: `Bearer ${apiKey}`,
73
+ "Mcp-Session-Id": sessionId,
74
+ },
75
+ body: JSON.stringify({
76
+ jsonrpc: "2.0",
77
+ id: 2,
78
+ method: "tools/call",
79
+ params: { name: toolName, arguments: args },
80
+ }),
81
+ });
82
+
83
+ if (!response.ok) {
84
+ throw new Error(
85
+ `MCP tools/call failed: ${response.status} ${response.statusText}`,
86
+ );
87
+ }
88
+
89
+ const raw = await response.text();
90
+ const parsed = parseSseResponse(raw);
91
+
92
+ if (parsed.error) {
93
+ throw new Error(`MCP error: ${parsed.error.message}`);
94
+ }
95
+
96
+ const contents = parsed.result?.content ?? [];
97
+ const full = contents.map((c) => c.text).join("\n");
98
+ const MAX_CONTENT_LENGTH = 30000;
99
+ if (full.length > MAX_CONTENT_LENGTH) {
100
+ return full.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Content truncated]";
101
+ }
102
+ return full;
103
+ }
104
+
105
+ let cachedSessionId: string | undefined;
106
+
107
+ export async function fetchWebContent(
108
+ config: GlobalConfig,
109
+ url: string,
110
+ ): Promise<string> {
111
+ const endpoint = config.webReader.url;
112
+ const apiKey = config.webReader.apiKey;
113
+
114
+ if (!cachedSessionId) {
115
+ cachedSessionId = await mcpInitialize(endpoint, apiKey);
116
+ }
117
+
118
+ try {
119
+ return await mcpToolCall(endpoint, apiKey, cachedSessionId, "webReader", {
120
+ url,
121
+ timeout: 20,
122
+ });
123
+ } catch {
124
+ cachedSessionId = await mcpInitialize(endpoint, apiKey);
125
+ return await mcpToolCall(endpoint, apiKey, cachedSessionId, "webReader", {
126
+ url,
127
+ timeout: 20,
128
+ });
129
+ }
130
+ }
package/src/types.ts ADDED
@@ -0,0 +1,59 @@
1
+ export interface StdioConfig {
2
+ readonly transport: "stdio";
3
+ readonly command: string;
4
+ readonly args: readonly string[];
5
+ readonly env: Readonly<Record<string, string>>;
6
+ }
7
+
8
+ export interface HttpConfig {
9
+ readonly transport: "http";
10
+ readonly url: string;
11
+ readonly headers: Readonly<Record<string, string>>;
12
+ }
13
+
14
+ export type DefaultConfig = StdioConfig | HttpConfig;
15
+
16
+ export type AgentId =
17
+ | "claude-code"
18
+ | "codex-cli"
19
+ | "gemini-cli"
20
+ | "opencode"
21
+ | "antigravity";
22
+
23
+ export interface ServerDefinition {
24
+ readonly name: string;
25
+ readonly source: string;
26
+ readonly default: DefaultConfig;
27
+ readonly overrides: Readonly<Partial<Record<AgentId, Partial<DefaultConfig>>>>;
28
+ }
29
+
30
+ export interface GlobalConfig {
31
+ readonly glm: {
32
+ readonly apiKey: string;
33
+ readonly endpoint: string;
34
+ };
35
+ readonly webReader: {
36
+ readonly apiKey: string;
37
+ readonly url: string;
38
+ };
39
+ }
40
+
41
+ export interface AgentAdapter {
42
+ readonly id: AgentId;
43
+ readonly name: string;
44
+ readonly configPath: (projectDir: string) => string;
45
+ readonly isGlobal: boolean;
46
+ read(projectDir: string): Promise<Record<string, unknown>>;
47
+ write(
48
+ projectDir: string,
49
+ serverName: string,
50
+ config: DefaultConfig,
51
+ ): Promise<void>;
52
+ remove(projectDir: string, serverName: string): Promise<void>;
53
+ has(projectDir: string, serverName: string): Promise<boolean>;
54
+ toAgentFormat(config: DefaultConfig): Record<string, unknown>;
55
+ fromAgentFormat(
56
+ name: string,
57
+ raw: Record<string, unknown>,
58
+ ): DefaultConfig | undefined;
59
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import type { GlobalConfig } from "../types.js";
4
+ import { paths } from "./paths.js";
5
+
6
+ export async function readGlobalConfig(): Promise<GlobalConfig> {
7
+ const raw = await readFile(paths.configFile, "utf-8");
8
+ return JSON.parse(raw) as GlobalConfig;
9
+ }
10
+
11
+ export async function writeGlobalConfig(config: GlobalConfig): Promise<void> {
12
+ if (!existsSync(paths.baseDir)) {
13
+ await mkdir(paths.baseDir, { recursive: true });
14
+ await chmod(paths.baseDir, 0o700);
15
+ }
16
+ await writeFile(paths.configFile, JSON.stringify(config, null, 2), "utf-8");
17
+ await chmod(paths.configFile, 0o600);
18
+ }
19
+
20
+ export function configExists(): boolean {
21
+ return existsSync(paths.configFile);
22
+ }
@@ -0,0 +1,11 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ const BASE_DIR = join(homedir(), ".mcps-manager");
5
+
6
+ export const paths = {
7
+ baseDir: BASE_DIR,
8
+ serversDir: join(BASE_DIR, "servers"),
9
+ configFile: join(BASE_DIR, "config.json"),
10
+ serverFile: (name: string): string => join(BASE_DIR, "servers", `${name}.json`),
11
+ } as const;
@@ -0,0 +1,3 @@
1
+ export function isUserCancellation(error: unknown): boolean {
2
+ return error instanceof Error && error.name === "ExitPromptError";
3
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentAdapter, DefaultConfig, ServerDefinition } from "../types.js";
2
+
3
+ export function resolveConfig(
4
+ definition: ServerDefinition,
5
+ adapter: AgentAdapter,
6
+ ): DefaultConfig {
7
+ const override = definition.overrides[adapter.id];
8
+ if (override) {
9
+ const base = definition.default;
10
+ return { ...base, ...override } as DefaultConfig;
11
+ }
12
+ return definition.default;
13
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile, writeFile, readdir, unlink, mkdir, chmod } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import type { ServerDefinition } from "../types.js";
4
+ import { paths } from "./paths.js";
5
+
6
+ export async function readServerDefinition(
7
+ name: string,
8
+ ): Promise<ServerDefinition | undefined> {
9
+ const filePath = paths.serverFile(name);
10
+ if (!existsSync(filePath)) {
11
+ return undefined;
12
+ }
13
+ const raw = await readFile(filePath, "utf-8");
14
+ return JSON.parse(raw) as ServerDefinition;
15
+ }
16
+
17
+ export async function writeServerDefinition(
18
+ definition: ServerDefinition,
19
+ ): Promise<void> {
20
+ if (!existsSync(paths.serversDir)) {
21
+ await mkdir(paths.serversDir, { recursive: true });
22
+ }
23
+ const filePath = paths.serverFile(definition.name);
24
+ await writeFile(filePath, JSON.stringify(definition, null, 2), "utf-8");
25
+ await chmod(filePath, 0o600);
26
+ }
27
+
28
+ export async function removeServerDefinition(name: string): Promise<boolean> {
29
+ const filePath = paths.serverFile(name);
30
+ if (!existsSync(filePath)) {
31
+ return false;
32
+ }
33
+ await unlink(filePath);
34
+ return true;
35
+ }
36
+
37
+ export async function listServerDefinitions(): Promise<ServerDefinition[]> {
38
+ if (!existsSync(paths.serversDir)) {
39
+ return [];
40
+ }
41
+ const files = await readdir(paths.serversDir);
42
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
43
+ const results: ServerDefinition[] = [];
44
+ for (const file of jsonFiles) {
45
+ const raw = await readFile(
46
+ paths.serverFile(file.replace(".json", "")),
47
+ "utf-8",
48
+ );
49
+ results.push(JSON.parse(raw) as ServerDefinition);
50
+ }
51
+ return results;
52
+ }
53
+
54
+ export function serverExists(name: string): boolean {
55
+ return existsSync(paths.serverFile(name));
56
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ target: "node20",
7
+ clean: true,
8
+ dts: true,
9
+ sourcemap: true,
10
+ banner: {
11
+ js: "#!/usr/bin/env node",
12
+ },
13
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ },
8
+ });