@clinebot/core 0.0.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 (200) hide show
  1. package/README.md +88 -0
  2. package/dist/account/cline-account-service.d.ts +34 -0
  3. package/dist/account/index.d.ts +3 -0
  4. package/dist/account/rpc.d.ts +38 -0
  5. package/dist/account/types.d.ts +74 -0
  6. package/dist/agents/agent-config-loader.d.ts +18 -0
  7. package/dist/agents/agent-config-parser.d.ts +25 -0
  8. package/dist/agents/hooks-config-loader.d.ts +23 -0
  9. package/dist/agents/index.d.ts +11 -0
  10. package/dist/agents/plugin-config-loader.d.ts +22 -0
  11. package/dist/agents/plugin-loader.d.ts +9 -0
  12. package/dist/agents/plugin-sandbox.d.ts +12 -0
  13. package/dist/agents/unified-config-file-watcher.d.ts +77 -0
  14. package/dist/agents/user-instruction-config-loader.d.ts +63 -0
  15. package/dist/auth/client.d.ts +11 -0
  16. package/dist/auth/cline.d.ts +41 -0
  17. package/dist/auth/codex.d.ts +39 -0
  18. package/dist/auth/oca.d.ts +22 -0
  19. package/dist/auth/server.d.ts +22 -0
  20. package/dist/auth/types.d.ts +72 -0
  21. package/dist/auth/utils.d.ts +32 -0
  22. package/dist/chat/chat-schema.d.ts +145 -0
  23. package/dist/default-tools/constants.d.ts +23 -0
  24. package/dist/default-tools/definitions.d.ts +96 -0
  25. package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
  26. package/dist/default-tools/executors/apply-patch.d.ts +26 -0
  27. package/dist/default-tools/executors/bash.d.ts +49 -0
  28. package/dist/default-tools/executors/editor.d.ts +31 -0
  29. package/dist/default-tools/executors/file-read.d.ts +40 -0
  30. package/dist/default-tools/executors/index.d.ts +44 -0
  31. package/dist/default-tools/executors/search.d.ts +50 -0
  32. package/dist/default-tools/executors/web-fetch.d.ts +58 -0
  33. package/dist/default-tools/index.d.ts +57 -0
  34. package/dist/default-tools/presets.d.ts +124 -0
  35. package/dist/default-tools/schemas.d.ts +121 -0
  36. package/dist/default-tools/types.d.ts +237 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +220 -0
  39. package/dist/input/file-indexer.d.ts +5 -0
  40. package/dist/input/index.d.ts +4 -0
  41. package/dist/input/mention-enricher.d.ts +12 -0
  42. package/dist/mcp/config-loader.d.ts +15 -0
  43. package/dist/mcp/index.d.ts +4 -0
  44. package/dist/mcp/manager.d.ts +24 -0
  45. package/dist/mcp/types.d.ts +66 -0
  46. package/dist/runtime/hook-file-hooks.d.ts +18 -0
  47. package/dist/runtime/rules.d.ts +5 -0
  48. package/dist/runtime/runtime-builder.d.ts +5 -0
  49. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
  50. package/dist/runtime/session-runtime.d.ts +36 -0
  51. package/dist/runtime/tool-approval.d.ts +9 -0
  52. package/dist/runtime/workflows.d.ts +13 -0
  53. package/dist/server/index.d.ts +47 -0
  54. package/dist/server/index.js +641 -0
  55. package/dist/session/default-session-manager.d.ts +77 -0
  56. package/dist/session/rpc-session-service.d.ts +12 -0
  57. package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
  58. package/dist/session/session-artifacts.d.ts +19 -0
  59. package/dist/session/session-graph.d.ts +15 -0
  60. package/dist/session/session-host.d.ts +21 -0
  61. package/dist/session/session-manager.d.ts +50 -0
  62. package/dist/session/session-manifest.d.ts +30 -0
  63. package/dist/session/session-service.d.ts +113 -0
  64. package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
  65. package/dist/session/unified-session-persistence-service.d.ts +93 -0
  66. package/dist/session/workspace-manager.d.ts +28 -0
  67. package/dist/session/workspace-manifest.d.ts +25 -0
  68. package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
  69. package/dist/storage/provider-settings-manager.d.ts +20 -0
  70. package/dist/storage/sqlite-session-store.d.ts +29 -0
  71. package/dist/storage/sqlite-team-store.d.ts +31 -0
  72. package/dist/storage/team-store.d.ts +2 -0
  73. package/dist/team/index.d.ts +1 -0
  74. package/dist/team/projections.d.ts +8 -0
  75. package/dist/types/common.d.ts +10 -0
  76. package/dist/types/config.d.ts +37 -0
  77. package/dist/types/events.d.ts +54 -0
  78. package/dist/types/provider-settings.d.ts +20 -0
  79. package/dist/types/sessions.d.ts +9 -0
  80. package/dist/types/storage.d.ts +37 -0
  81. package/dist/types/workspace.d.ts +7 -0
  82. package/dist/types.d.ts +26 -0
  83. package/package.json +63 -0
  84. package/src/account/cline-account-service.test.ts +101 -0
  85. package/src/account/cline-account-service.ts +267 -0
  86. package/src/account/index.ts +20 -0
  87. package/src/account/rpc.test.ts +62 -0
  88. package/src/account/rpc.ts +172 -0
  89. package/src/account/types.ts +80 -0
  90. package/src/agents/agent-config-loader.test.ts +234 -0
  91. package/src/agents/agent-config-loader.ts +107 -0
  92. package/src/agents/agent-config-parser.ts +191 -0
  93. package/src/agents/hooks-config-loader.ts +97 -0
  94. package/src/agents/index.ts +84 -0
  95. package/src/agents/plugin-config-loader.test.ts +91 -0
  96. package/src/agents/plugin-config-loader.ts +160 -0
  97. package/src/agents/plugin-loader.test.ts +102 -0
  98. package/src/agents/plugin-loader.ts +105 -0
  99. package/src/agents/plugin-sandbox.test.ts +120 -0
  100. package/src/agents/plugin-sandbox.ts +471 -0
  101. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  102. package/src/agents/unified-config-file-watcher.ts +483 -0
  103. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  104. package/src/agents/user-instruction-config-loader.ts +438 -0
  105. package/src/auth/client.test.ts +40 -0
  106. package/src/auth/client.ts +25 -0
  107. package/src/auth/cline.test.ts +130 -0
  108. package/src/auth/cline.ts +414 -0
  109. package/src/auth/codex.test.ts +170 -0
  110. package/src/auth/codex.ts +466 -0
  111. package/src/auth/oca.test.ts +215 -0
  112. package/src/auth/oca.ts +546 -0
  113. package/src/auth/server.ts +216 -0
  114. package/src/auth/types.ts +78 -0
  115. package/src/auth/utils.test.ts +128 -0
  116. package/src/auth/utils.ts +247 -0
  117. package/src/chat/chat-schema.ts +82 -0
  118. package/src/default-tools/constants.ts +35 -0
  119. package/src/default-tools/definitions.test.ts +233 -0
  120. package/src/default-tools/definitions.ts +632 -0
  121. package/src/default-tools/executors/apply-patch-parser.ts +520 -0
  122. package/src/default-tools/executors/apply-patch.ts +359 -0
  123. package/src/default-tools/executors/bash.ts +205 -0
  124. package/src/default-tools/executors/editor.ts +231 -0
  125. package/src/default-tools/executors/file-read.test.ts +25 -0
  126. package/src/default-tools/executors/file-read.ts +94 -0
  127. package/src/default-tools/executors/index.ts +75 -0
  128. package/src/default-tools/executors/search.ts +278 -0
  129. package/src/default-tools/executors/web-fetch.ts +259 -0
  130. package/src/default-tools/index.ts +161 -0
  131. package/src/default-tools/presets.test.ts +63 -0
  132. package/src/default-tools/presets.ts +168 -0
  133. package/src/default-tools/schemas.ts +228 -0
  134. package/src/default-tools/types.ts +324 -0
  135. package/src/index.ts +119 -0
  136. package/src/input/file-indexer.d.ts +11 -0
  137. package/src/input/file-indexer.test.ts +87 -0
  138. package/src/input/file-indexer.ts +280 -0
  139. package/src/input/index.ts +7 -0
  140. package/src/input/mention-enricher.test.ts +82 -0
  141. package/src/input/mention-enricher.ts +119 -0
  142. package/src/mcp/config-loader.test.ts +238 -0
  143. package/src/mcp/config-loader.ts +219 -0
  144. package/src/mcp/index.ts +26 -0
  145. package/src/mcp/manager.test.ts +106 -0
  146. package/src/mcp/manager.ts +262 -0
  147. package/src/mcp/types.ts +88 -0
  148. package/src/runtime/hook-file-hooks.test.ts +106 -0
  149. package/src/runtime/hook-file-hooks.ts +736 -0
  150. package/src/runtime/index.ts +27 -0
  151. package/src/runtime/rules.ts +34 -0
  152. package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
  153. package/src/runtime/runtime-builder.test.ts +215 -0
  154. package/src/runtime/runtime-builder.ts +515 -0
  155. package/src/runtime/runtime-parity.test.ts +132 -0
  156. package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
  157. package/src/runtime/session-runtime.ts +44 -0
  158. package/src/runtime/tool-approval.ts +104 -0
  159. package/src/runtime/workflows.test.ts +119 -0
  160. package/src/runtime/workflows.ts +54 -0
  161. package/src/server/index.ts +282 -0
  162. package/src/session/default-session-manager.e2e.test.ts +354 -0
  163. package/src/session/default-session-manager.test.ts +816 -0
  164. package/src/session/default-session-manager.ts +1286 -0
  165. package/src/session/index.ts +37 -0
  166. package/src/session/rpc-session-service.ts +189 -0
  167. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  168. package/src/session/runtime-oauth-token-manager.ts +265 -0
  169. package/src/session/session-artifacts.ts +106 -0
  170. package/src/session/session-graph.ts +90 -0
  171. package/src/session/session-host.ts +190 -0
  172. package/src/session/session-manager.ts +56 -0
  173. package/src/session/session-manifest.ts +29 -0
  174. package/src/session/session-service.team-persistence.test.ts +48 -0
  175. package/src/session/session-service.ts +610 -0
  176. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  177. package/src/session/unified-session-persistence-service.ts +781 -0
  178. package/src/session/workspace-manager.ts +98 -0
  179. package/src/session/workspace-manifest.ts +100 -0
  180. package/src/storage/artifact-store.ts +1 -0
  181. package/src/storage/index.ts +11 -0
  182. package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
  183. package/src/storage/provider-settings-legacy-migration.ts +637 -0
  184. package/src/storage/provider-settings-manager.test.ts +111 -0
  185. package/src/storage/provider-settings-manager.ts +129 -0
  186. package/src/storage/session-store.ts +1 -0
  187. package/src/storage/sqlite-session-store.ts +270 -0
  188. package/src/storage/sqlite-team-store.ts +443 -0
  189. package/src/storage/team-store.ts +5 -0
  190. package/src/team/index.ts +4 -0
  191. package/src/team/projections.ts +285 -0
  192. package/src/types/common.ts +14 -0
  193. package/src/types/config.ts +64 -0
  194. package/src/types/events.ts +46 -0
  195. package/src/types/index.ts +24 -0
  196. package/src/types/provider-settings.ts +43 -0
  197. package/src/types/sessions.ts +16 -0
  198. package/src/types/storage.ts +64 -0
  199. package/src/types/workspace.ts +7 -0
  200. package/src/types.ts +127 -0
@@ -0,0 +1,238 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import {
6
+ hasMcpSettingsFile,
7
+ loadMcpSettingsFile,
8
+ registerMcpServersFromSettingsFile,
9
+ resolveMcpServerRegistrations,
10
+ } from "./config-loader";
11
+
12
+ describe("mcp config loader", () => {
13
+ const tempRoots: string[] = [];
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(
17
+ tempRoots.map((directory) =>
18
+ rm(directory, { recursive: true, force: true }),
19
+ ),
20
+ );
21
+ tempRoots.length = 0;
22
+ });
23
+
24
+ it("loads and validates mcp server registrations from JSON", async () => {
25
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-mcp-config-loader-"));
26
+ tempRoots.push(tempRoot);
27
+ const filePath = join(tempRoot, "cline_mcp_settings.json");
28
+ await writeFile(
29
+ filePath,
30
+ JSON.stringify(
31
+ {
32
+ mcpServers: {
33
+ docs: {
34
+ transport: {
35
+ type: "stdio",
36
+ command: "npx",
37
+ args: ["-y", "@modelcontextprotocol/server-filesystem"],
38
+ },
39
+ },
40
+ search: {
41
+ transport: {
42
+ type: "streamableHttp",
43
+ url: "https://mcp.example.com",
44
+ },
45
+ disabled: true,
46
+ },
47
+ },
48
+ },
49
+ null,
50
+ 2,
51
+ ),
52
+ "utf8",
53
+ );
54
+
55
+ expect(hasMcpSettingsFile({ filePath })).toBe(true);
56
+ expect(
57
+ loadMcpSettingsFile({ filePath }).mcpServers.docs.transport.type,
58
+ ).toBe("stdio");
59
+
60
+ const registrations = resolveMcpServerRegistrations({ filePath });
61
+ expect(registrations).toEqual([
62
+ {
63
+ name: "docs",
64
+ transport: {
65
+ type: "stdio",
66
+ command: "npx",
67
+ args: ["-y", "@modelcontextprotocol/server-filesystem"],
68
+ },
69
+ disabled: undefined,
70
+ metadata: undefined,
71
+ },
72
+ {
73
+ name: "search",
74
+ transport: {
75
+ type: "streamableHttp",
76
+ url: "https://mcp.example.com",
77
+ },
78
+ disabled: true,
79
+ metadata: undefined,
80
+ },
81
+ ]);
82
+ });
83
+
84
+ it("registers loaded servers with an mcp manager", async () => {
85
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-mcp-config-loader-"));
86
+ tempRoots.push(tempRoot);
87
+ const filePath = join(tempRoot, "cline_mcp_settings.json");
88
+ await writeFile(
89
+ filePath,
90
+ JSON.stringify(
91
+ {
92
+ mcpServers: {
93
+ docs: {
94
+ transport: {
95
+ type: "stdio",
96
+ command: "node",
97
+ },
98
+ },
99
+ },
100
+ },
101
+ null,
102
+ 2,
103
+ ),
104
+ "utf8",
105
+ );
106
+
107
+ const registered: Array<{ name: string }> = [];
108
+ const manager = {
109
+ registerServer: async (registration: { name: string }) => {
110
+ registered.push(registration);
111
+ },
112
+ };
113
+
114
+ await registerMcpServersFromSettingsFile(manager, { filePath });
115
+ expect(registered).toEqual([
116
+ {
117
+ name: "docs",
118
+ transport: {
119
+ type: "stdio",
120
+ command: "node",
121
+ },
122
+ disabled: undefined,
123
+ metadata: undefined,
124
+ },
125
+ ]);
126
+ });
127
+
128
+ it("throws a clear error for invalid config", async () => {
129
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-mcp-config-loader-"));
130
+ tempRoots.push(tempRoot);
131
+ const filePath = join(tempRoot, "cline_mcp_settings.json");
132
+ await writeFile(
133
+ filePath,
134
+ JSON.stringify(
135
+ {
136
+ mcpServers: {
137
+ broken: {
138
+ transport: {
139
+ type: "stdio",
140
+ command: "",
141
+ },
142
+ },
143
+ },
144
+ },
145
+ null,
146
+ 2,
147
+ ),
148
+ "utf8",
149
+ );
150
+
151
+ expect(() => resolveMcpServerRegistrations({ filePath })).toThrow(
152
+ "Invalid MCP settings",
153
+ );
154
+ });
155
+
156
+ it("accepts legacy flat stdio format", async () => {
157
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-mcp-config-loader-"));
158
+ tempRoots.push(tempRoot);
159
+ const filePath = join(tempRoot, "cline_mcp_settings.json");
160
+ await writeFile(
161
+ filePath,
162
+ JSON.stringify(
163
+ {
164
+ mcpServers: {
165
+ docs: {
166
+ command: "node",
167
+ args: ["server.js"],
168
+ },
169
+ },
170
+ },
171
+ null,
172
+ 2,
173
+ ),
174
+ "utf8",
175
+ );
176
+
177
+ const registrations = resolveMcpServerRegistrations({ filePath });
178
+ expect(registrations).toEqual([
179
+ {
180
+ name: "docs",
181
+ transport: {
182
+ type: "stdio",
183
+ command: "node",
184
+ args: ["server.js"],
185
+ },
186
+ disabled: undefined,
187
+ metadata: undefined,
188
+ },
189
+ ]);
190
+ });
191
+
192
+ it("accepts legacy flat url format and preserves explicit transportType", async () => {
193
+ const tempRoot = await mkdtemp(join(tmpdir(), "core-mcp-config-loader-"));
194
+ tempRoots.push(tempRoot);
195
+ const filePath = join(tempRoot, "cline_mcp_settings.json");
196
+ await writeFile(
197
+ filePath,
198
+ JSON.stringify(
199
+ {
200
+ mcpServers: {
201
+ legacySse: {
202
+ url: "https://sse.example.com",
203
+ },
204
+ legacyHttp: {
205
+ url: "https://http.example.com",
206
+ transportType: "http",
207
+ },
208
+ },
209
+ },
210
+ null,
211
+ 2,
212
+ ),
213
+ "utf8",
214
+ );
215
+
216
+ const registrations = resolveMcpServerRegistrations({ filePath });
217
+ expect(registrations).toEqual([
218
+ {
219
+ name: "legacySse",
220
+ transport: {
221
+ type: "sse",
222
+ url: "https://sse.example.com",
223
+ },
224
+ disabled: undefined,
225
+ metadata: undefined,
226
+ },
227
+ {
228
+ name: "legacyHttp",
229
+ transport: {
230
+ type: "streamableHttp",
231
+ url: "https://http.example.com",
232
+ },
233
+ disabled: undefined,
234
+ metadata: undefined,
235
+ },
236
+ ]);
237
+ });
238
+ });
@@ -0,0 +1,219 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolveMcpSettingsPath } from "@clinebot/shared/storage";
3
+ import { z } from "zod";
4
+ import type { McpManager, McpServerRegistration } from "./types";
5
+
6
+ const stringRecordSchema = z.record(z.string(), z.string());
7
+ const metadataSchema = z.record(z.string(), z.unknown());
8
+
9
+ const stdioTransportSchema = z.object({
10
+ type: z.literal("stdio"),
11
+ command: z.string().min(1),
12
+ args: z.array(z.string()).optional(),
13
+ cwd: z.string().min(1).optional(),
14
+ env: stringRecordSchema.optional(),
15
+ });
16
+
17
+ const sseTransportSchema = z.object({
18
+ type: z.literal("sse"),
19
+ url: z.string().url(),
20
+ headers: stringRecordSchema.optional(),
21
+ });
22
+
23
+ const streamableHttpTransportSchema = z.object({
24
+ type: z.literal("streamableHttp"),
25
+ url: z.string().url(),
26
+ headers: stringRecordSchema.optional(),
27
+ });
28
+
29
+ const mcpTransportSchema = z.discriminatedUnion("type", [
30
+ stdioTransportSchema,
31
+ sseTransportSchema,
32
+ streamableHttpTransportSchema,
33
+ ]);
34
+
35
+ const nestedRegistrationBodySchema = z.object({
36
+ transport: mcpTransportSchema,
37
+ disabled: z.boolean().optional(),
38
+ metadata: metadataSchema.optional(),
39
+ });
40
+
41
+ const legacyTransportTypeSchema = z
42
+ .enum(["stdio", "sse", "http", "streamableHttp"])
43
+ .optional();
44
+
45
+ const legacyRegistrationBaseSchema = z.object({
46
+ type: z.enum(["stdio", "sse", "streamableHttp"]).optional(),
47
+ transportType: legacyTransportTypeSchema,
48
+ disabled: z.boolean().optional(),
49
+ metadata: metadataSchema.optional(),
50
+ });
51
+
52
+ function mapLegacyTransportType(
53
+ transportType: z.infer<typeof legacyTransportTypeSchema>,
54
+ ): "stdio" | "sse" | "streamableHttp" | undefined {
55
+ if (!transportType) {
56
+ return undefined;
57
+ }
58
+ if (transportType === "http") {
59
+ return "streamableHttp";
60
+ }
61
+ return transportType;
62
+ }
63
+
64
+ const legacyStdioRegistrationSchema = legacyRegistrationBaseSchema
65
+ .extend({
66
+ command: z.string().min(1),
67
+ args: z.array(z.string()).optional(),
68
+ cwd: z.string().min(1).optional(),
69
+ env: stringRecordSchema.optional(),
70
+ })
71
+ .superRefine((value, ctx) => {
72
+ const resolvedType =
73
+ value.type ?? mapLegacyTransportType(value.transportType);
74
+ if (resolvedType && resolvedType !== "stdio") {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ message: 'Expected type "stdio" for command-based MCP server',
78
+ path: ["type"],
79
+ });
80
+ }
81
+ })
82
+ .transform((value) => ({
83
+ transport: {
84
+ type: "stdio" as const,
85
+ command: value.command,
86
+ args: value.args,
87
+ cwd: value.cwd,
88
+ env: value.env,
89
+ },
90
+ disabled: value.disabled,
91
+ metadata: value.metadata,
92
+ }));
93
+
94
+ const legacyUrlRegistrationSchema = legacyRegistrationBaseSchema
95
+ .extend({
96
+ url: z.string().url(),
97
+ headers: stringRecordSchema.optional(),
98
+ })
99
+ .superRefine((value, ctx) => {
100
+ const resolvedType =
101
+ value.type ?? mapLegacyTransportType(value.transportType) ?? "sse";
102
+ if (resolvedType !== "sse" && resolvedType !== "streamableHttp") {
103
+ ctx.addIssue({
104
+ code: z.ZodIssueCode.custom,
105
+ message:
106
+ 'Expected type "sse" or "streamableHttp" for URL-based MCP server',
107
+ path: ["type"],
108
+ });
109
+ }
110
+ })
111
+ .transform((value) => {
112
+ const resolvedType =
113
+ value.type ?? mapLegacyTransportType(value.transportType) ?? "sse";
114
+ if (resolvedType === "streamableHttp") {
115
+ return {
116
+ transport: {
117
+ type: "streamableHttp" as const,
118
+ url: value.url,
119
+ headers: value.headers,
120
+ },
121
+ disabled: value.disabled,
122
+ metadata: value.metadata,
123
+ };
124
+ }
125
+ return {
126
+ transport: {
127
+ type: "sse" as const,
128
+ url: value.url,
129
+ headers: value.headers,
130
+ },
131
+ disabled: value.disabled,
132
+ metadata: value.metadata,
133
+ };
134
+ });
135
+
136
+ const mcpRegistrationBodySchema = z.union([
137
+ nestedRegistrationBodySchema,
138
+ legacyStdioRegistrationSchema,
139
+ legacyUrlRegistrationSchema,
140
+ ]);
141
+
142
+ const mcpSettingsSchema = z
143
+ .object({
144
+ mcpServers: z.record(z.string(), mcpRegistrationBodySchema),
145
+ })
146
+ .strict();
147
+
148
+ export interface McpSettingsFile {
149
+ mcpServers: Record<string, Omit<McpServerRegistration, "name">>;
150
+ }
151
+
152
+ export interface LoadMcpSettingsOptions {
153
+ filePath?: string;
154
+ }
155
+
156
+ export interface RegisterMcpServersFromSettingsOptions {
157
+ filePath?: string;
158
+ }
159
+
160
+ export function resolveDefaultMcpSettingsPath(): string {
161
+ return resolveMcpSettingsPath();
162
+ }
163
+
164
+ export function loadMcpSettingsFile(
165
+ options: LoadMcpSettingsOptions = {},
166
+ ): McpSettingsFile {
167
+ const filePath = options.filePath ?? resolveDefaultMcpSettingsPath();
168
+ const raw = readFileSync(filePath, "utf8");
169
+ let parsed: unknown;
170
+ try {
171
+ parsed = JSON.parse(raw);
172
+ } catch (error) {
173
+ const details = error instanceof Error ? error.message : String(error);
174
+ throw new Error(
175
+ `Failed to parse MCP settings JSON at "${filePath}": ${details}`,
176
+ );
177
+ }
178
+ const result = mcpSettingsSchema.safeParse(parsed);
179
+ if (!result.success) {
180
+ const details = result.error.issues
181
+ .map((issue) => {
182
+ const path = issue.path.join(".");
183
+ return path ? `${path}: ${issue.message}` : issue.message;
184
+ })
185
+ .join("; ");
186
+ throw new Error(`Invalid MCP settings at "${filePath}": ${details}`);
187
+ }
188
+ return result.data;
189
+ }
190
+
191
+ export function hasMcpSettingsFile(
192
+ options: LoadMcpSettingsOptions = {},
193
+ ): boolean {
194
+ const filePath = options.filePath ?? resolveDefaultMcpSettingsPath();
195
+ return existsSync(filePath);
196
+ }
197
+
198
+ export function resolveMcpServerRegistrations(
199
+ options: LoadMcpSettingsOptions = {},
200
+ ): McpServerRegistration[] {
201
+ const config = loadMcpSettingsFile(options);
202
+ return Object.entries(config.mcpServers).map(([name, value]) => ({
203
+ name,
204
+ transport: value.transport,
205
+ disabled: value.disabled,
206
+ metadata: value.metadata,
207
+ }));
208
+ }
209
+
210
+ export async function registerMcpServersFromSettingsFile(
211
+ manager: Pick<McpManager, "registerServer">,
212
+ options: RegisterMcpServersFromSettingsOptions = {},
213
+ ): Promise<McpServerRegistration[]> {
214
+ const registrations = resolveMcpServerRegistrations(options);
215
+ for (const registration of registrations) {
216
+ await manager.registerServer(registration);
217
+ }
218
+ return registrations;
219
+ }
@@ -0,0 +1,26 @@
1
+ export type {
2
+ LoadMcpSettingsOptions,
3
+ McpSettingsFile,
4
+ RegisterMcpServersFromSettingsOptions,
5
+ } from "./config-loader";
6
+ export {
7
+ hasMcpSettingsFile,
8
+ loadMcpSettingsFile,
9
+ registerMcpServersFromSettingsFile,
10
+ resolveDefaultMcpSettingsPath,
11
+ resolveMcpServerRegistrations,
12
+ } from "./config-loader";
13
+ export { InMemoryMcpManager } from "./manager";
14
+ export type {
15
+ McpConnectionStatus,
16
+ McpManager,
17
+ McpManagerOptions,
18
+ McpServerClient,
19
+ McpServerClientFactory,
20
+ McpServerRegistration,
21
+ McpServerSnapshot,
22
+ McpServerTransportConfig,
23
+ McpSseTransportConfig,
24
+ McpStdioTransportConfig,
25
+ McpStreamableHttpTransportConfig,
26
+ } from "./types";
@@ -0,0 +1,106 @@
1
+ import type { McpToolDescriptor } from "@clinebot/agents";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { InMemoryMcpManager } from "./manager";
4
+ import type { McpServerClient } from "./types";
5
+
6
+ function createClient(overrides?: Partial<McpServerClient>): McpServerClient {
7
+ return {
8
+ connect: vi.fn(async () => {}),
9
+ disconnect: vi.fn(async () => {}),
10
+ listTools: vi.fn(async () => []),
11
+ callTool: vi.fn(async () => ({ ok: true })),
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe("InMemoryMcpManager", () => {
17
+ it("registers servers, connects on demand, and calls tools", async () => {
18
+ const toolDescriptors: readonly McpToolDescriptor[] = [
19
+ {
20
+ name: "search",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: { q: { type: "string" } },
24
+ required: ["q"],
25
+ },
26
+ },
27
+ ];
28
+ const client = createClient({
29
+ listTools: vi.fn(async () => toolDescriptors),
30
+ });
31
+ const manager = new InMemoryMcpManager({
32
+ clientFactory: vi.fn(async () => client),
33
+ });
34
+
35
+ await manager.registerServer({
36
+ name: "docs",
37
+ transport: {
38
+ type: "streamableHttp",
39
+ url: "https://mcp.example.test",
40
+ },
41
+ });
42
+
43
+ const tools = await manager.listTools("docs");
44
+ expect(tools).toHaveLength(1);
45
+
46
+ await manager.callTool({
47
+ serverName: "docs",
48
+ toolName: "search",
49
+ arguments: { q: "oauth flow" },
50
+ });
51
+
52
+ expect(client.connect).toHaveBeenCalledTimes(1);
53
+ expect(client.callTool).toHaveBeenCalledWith({
54
+ name: "search",
55
+ arguments: { q: "oauth flow" },
56
+ context: undefined,
57
+ });
58
+ });
59
+
60
+ it("uses tool list cache to avoid repeated listTools round trips", async () => {
61
+ const toolDescriptors: readonly McpToolDescriptor[] = [
62
+ {
63
+ name: "echo",
64
+ inputSchema: { type: "object", properties: {} },
65
+ },
66
+ ];
67
+ const client = createClient({
68
+ listTools: vi.fn(async () => toolDescriptors),
69
+ });
70
+ const manager = new InMemoryMcpManager({
71
+ clientFactory: async () => client,
72
+ toolsCacheTtlMs: 60_000,
73
+ });
74
+
75
+ await manager.registerServer({
76
+ name: "cache-test",
77
+ transport: {
78
+ type: "stdio",
79
+ command: "node",
80
+ args: ["./mcp.js"],
81
+ },
82
+ });
83
+
84
+ await manager.listTools("cache-test");
85
+ await manager.listTools("cache-test");
86
+ expect(client.listTools).toHaveBeenCalledTimes(1);
87
+ });
88
+
89
+ it("prevents tool calls on disabled servers", async () => {
90
+ const manager = new InMemoryMcpManager({
91
+ clientFactory: async () => createClient(),
92
+ });
93
+ await manager.registerServer({
94
+ name: "disabled",
95
+ transport: { type: "sse", url: "https://example.test/sse" },
96
+ disabled: true,
97
+ });
98
+
99
+ await expect(
100
+ manager.callTool({
101
+ serverName: "disabled",
102
+ toolName: "anything",
103
+ }),
104
+ ).rejects.toThrow(/disabled/i);
105
+ });
106
+ });