toolhome 0.5.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/README.zh.md +281 -0
  4. package/dist/server/app.d.ts +16 -0
  5. package/dist/server/app.d.ts.map +1 -0
  6. package/dist/server/app.js +166 -0
  7. package/dist/server/app.js.map +1 -0
  8. package/dist/server/cli/main.d.ts +3 -0
  9. package/dist/server/cli/main.d.ts.map +1 -0
  10. package/dist/server/cli/main.js +587 -0
  11. package/dist/server/cli/main.js.map +1 -0
  12. package/dist/server/cli-plane/allow-list.d.ts +18 -0
  13. package/dist/server/cli-plane/allow-list.d.ts.map +1 -0
  14. package/dist/server/cli-plane/allow-list.js +28 -0
  15. package/dist/server/cli-plane/allow-list.js.map +1 -0
  16. package/dist/server/cli-plane/cli-service.d.ts +41 -0
  17. package/dist/server/cli-plane/cli-service.d.ts.map +1 -0
  18. package/dist/server/cli-plane/cli-service.js +282 -0
  19. package/dist/server/cli-plane/cli-service.js.map +1 -0
  20. package/dist/server/cli-plane/frames.d.ts +26 -0
  21. package/dist/server/cli-plane/frames.d.ts.map +1 -0
  22. package/dist/server/cli-plane/frames.js +12 -0
  23. package/dist/server/cli-plane/frames.js.map +1 -0
  24. package/dist/server/cli-plane/models.d.ts +174 -0
  25. package/dist/server/cli-plane/models.d.ts.map +1 -0
  26. package/dist/server/cli-plane/models.js +169 -0
  27. package/dist/server/cli-plane/models.js.map +1 -0
  28. package/dist/server/cli-plane/routes.d.ts +32 -0
  29. package/dist/server/cli-plane/routes.d.ts.map +1 -0
  30. package/dist/server/cli-plane/routes.js +94 -0
  31. package/dist/server/cli-plane/routes.js.map +1 -0
  32. package/dist/server/cli-plane/runner.d.ts +41 -0
  33. package/dist/server/cli-plane/runner.d.ts.map +1 -0
  34. package/dist/server/cli-plane/runner.js +163 -0
  35. package/dist/server/cli-plane/runner.js.map +1 -0
  36. package/dist/server/cli-plane/status.d.ts +12 -0
  37. package/dist/server/cli-plane/status.d.ts.map +1 -0
  38. package/dist/server/cli-plane/status.js +24 -0
  39. package/dist/server/cli-plane/status.js.map +1 -0
  40. package/dist/server/config/harness-import.d.ts +44 -0
  41. package/dist/server/config/harness-import.d.ts.map +1 -0
  42. package/dist/server/config/harness-import.js +166 -0
  43. package/dist/server/config/harness-import.js.map +1 -0
  44. package/dist/server/config.d.ts +20 -0
  45. package/dist/server/config.d.ts.map +1 -0
  46. package/dist/server/config.js +80 -0
  47. package/dist/server/config.js.map +1 -0
  48. package/dist/server/control/client.d.ts +24 -0
  49. package/dist/server/control/client.d.ts.map +1 -0
  50. package/dist/server/control/client.js +115 -0
  51. package/dist/server/control/client.js.map +1 -0
  52. package/dist/server/control/control-api.d.ts +39 -0
  53. package/dist/server/control/control-api.d.ts.map +1 -0
  54. package/dist/server/control/control-api.js +291 -0
  55. package/dist/server/control/control-api.js.map +1 -0
  56. package/dist/server/control/control-service.d.ts +713 -0
  57. package/dist/server/control/control-service.d.ts.map +1 -0
  58. package/dist/server/control/control-service.js +693 -0
  59. package/dist/server/control/control-service.js.map +1 -0
  60. package/dist/server/control/openapi.d.ts +2 -0
  61. package/dist/server/control/openapi.d.ts.map +1 -0
  62. package/dist/server/control/openapi.js +370 -0
  63. package/dist/server/control/openapi.js.map +1 -0
  64. package/dist/server/data-plane/data-plane.d.ts +20 -0
  65. package/dist/server/data-plane/data-plane.d.ts.map +1 -0
  66. package/dist/server/data-plane/data-plane.js +335 -0
  67. package/dist/server/data-plane/data-plane.js.map +1 -0
  68. package/dist/server/data-plane/gateway-server.d.ts +13 -0
  69. package/dist/server/data-plane/gateway-server.d.ts.map +1 -0
  70. package/dist/server/data-plane/gateway-server.js +796 -0
  71. package/dist/server/data-plane/gateway-server.js.map +1 -0
  72. package/dist/server/data-plane/legacy-session.d.ts +18 -0
  73. package/dist/server/data-plane/legacy-session.d.ts.map +1 -0
  74. package/dist/server/data-plane/legacy-session.js +137 -0
  75. package/dist/server/data-plane/legacy-session.js.map +1 -0
  76. package/dist/server/data-plane/projection.d.ts +18 -0
  77. package/dist/server/data-plane/projection.d.ts.map +1 -0
  78. package/dist/server/data-plane/projection.js +44 -0
  79. package/dist/server/data-plane/projection.js.map +1 -0
  80. package/dist/server/data-plane/registry.d.ts +31 -0
  81. package/dist/server/data-plane/registry.d.ts.map +1 -0
  82. package/dist/server/data-plane/registry.js +129 -0
  83. package/dist/server/data-plane/registry.js.map +1 -0
  84. package/dist/server/data-plane/task-extension.d.ts +7 -0
  85. package/dist/server/data-plane/task-extension.d.ts.map +1 -0
  86. package/dist/server/data-plane/task-extension.js +33 -0
  87. package/dist/server/data-plane/task-extension.js.map +1 -0
  88. package/dist/server/data-plane/virtualization.d.ts +35 -0
  89. package/dist/server/data-plane/virtualization.d.ts.map +1 -0
  90. package/dist/server/data-plane/virtualization.js +228 -0
  91. package/dist/server/data-plane/virtualization.js.map +1 -0
  92. package/dist/server/domain/errors.d.ts +9 -0
  93. package/dist/server/domain/errors.d.ts.map +1 -0
  94. package/dist/server/domain/errors.js +19 -0
  95. package/dist/server/domain/errors.js.map +1 -0
  96. package/dist/server/domain/models.d.ts +720 -0
  97. package/dist/server/domain/models.d.ts.map +1 -0
  98. package/dist/server/domain/models.js +327 -0
  99. package/dist/server/domain/models.js.map +1 -0
  100. package/dist/server/main.d.ts +2 -0
  101. package/dist/server/main.d.ts.map +1 -0
  102. package/dist/server/main.js +37 -0
  103. package/dist/server/main.js.map +1 -0
  104. package/dist/server/manage/management-mcp.d.ts +27 -0
  105. package/dist/server/manage/management-mcp.d.ts.map +1 -0
  106. package/dist/server/manage/management-mcp.js +431 -0
  107. package/dist/server/manage/management-mcp.js.map +1 -0
  108. package/dist/server/market/catalog.d.ts +111 -0
  109. package/dist/server/market/catalog.d.ts.map +1 -0
  110. package/dist/server/market/catalog.js +784 -0
  111. package/dist/server/market/catalog.js.map +1 -0
  112. package/dist/server/market/installers.d.ts +64 -0
  113. package/dist/server/market/installers.d.ts.map +1 -0
  114. package/dist/server/market/installers.js +181 -0
  115. package/dist/server/market/installers.js.map +1 -0
  116. package/dist/server/market/market-service.d.ts +146 -0
  117. package/dist/server/market/market-service.d.ts.map +1 -0
  118. package/dist/server/market/market-service.js +930 -0
  119. package/dist/server/market/market-service.js.map +1 -0
  120. package/dist/server/observability/call-recorder.d.ts +22 -0
  121. package/dist/server/observability/call-recorder.d.ts.map +1 -0
  122. package/dist/server/observability/call-recorder.js +99 -0
  123. package/dist/server/observability/call-recorder.js.map +1 -0
  124. package/dist/server/observability/logger.d.ts +16 -0
  125. package/dist/server/observability/logger.d.ts.map +1 -0
  126. package/dist/server/observability/logger.js +25 -0
  127. package/dist/server/observability/logger.js.map +1 -0
  128. package/dist/server/security/api-keys.d.ts +15 -0
  129. package/dist/server/security/api-keys.d.ts.map +1 -0
  130. package/dist/server/security/api-keys.js +35 -0
  131. package/dist/server/security/api-keys.js.map +1 -0
  132. package/dist/server/security/auth-service.d.ts +26 -0
  133. package/dist/server/security/auth-service.d.ts.map +1 -0
  134. package/dist/server/security/auth-service.js +76 -0
  135. package/dist/server/security/auth-service.js.map +1 -0
  136. package/dist/server/security/control-session.d.ts +9 -0
  137. package/dist/server/security/control-session.d.ts.map +1 -0
  138. package/dist/server/security/control-session.js +51 -0
  139. package/dist/server/security/control-session.js.map +1 -0
  140. package/dist/server/security/credential-materializer.d.ts +20 -0
  141. package/dist/server/security/credential-materializer.d.ts.map +1 -0
  142. package/dist/server/security/credential-materializer.js +82 -0
  143. package/dist/server/security/credential-materializer.js.map +1 -0
  144. package/dist/server/security/cursor-codec.d.ts +13 -0
  145. package/dist/server/security/cursor-codec.d.ts.map +1 -0
  146. package/dist/server/security/cursor-codec.js +43 -0
  147. package/dist/server/security/cursor-codec.js.map +1 -0
  148. package/dist/server/security/oauth-routes.d.ts +21 -0
  149. package/dist/server/security/oauth-routes.d.ts.map +1 -0
  150. package/dist/server/security/oauth-routes.js +100 -0
  151. package/dist/server/security/oauth-routes.js.map +1 -0
  152. package/dist/server/security/oauth-server.d.ts +16 -0
  153. package/dist/server/security/oauth-server.d.ts.map +1 -0
  154. package/dist/server/security/oauth-server.js +544 -0
  155. package/dist/server/security/oauth-server.js.map +1 -0
  156. package/dist/server/security/secret-box.d.ts +7 -0
  157. package/dist/server/security/secret-box.d.ts.map +1 -0
  158. package/dist/server/security/secret-box.js +27 -0
  159. package/dist/server/security/secret-box.js.map +1 -0
  160. package/dist/server/security/secure-action.d.ts +21 -0
  161. package/dist/server/security/secure-action.d.ts.map +1 -0
  162. package/dist/server/security/secure-action.js +69 -0
  163. package/dist/server/security/secure-action.js.map +1 -0
  164. package/dist/server/storage/sqlite-store.d.ts +70 -0
  165. package/dist/server/storage/sqlite-store.d.ts.map +1 -0
  166. package/dist/server/storage/sqlite-store.js +1522 -0
  167. package/dist/server/storage/sqlite-store.js.map +1 -0
  168. package/dist/server/storage/store.d.ts +82 -0
  169. package/dist/server/storage/store.d.ts.map +1 -0
  170. package/dist/server/storage/store.js +2 -0
  171. package/dist/server/storage/store.js.map +1 -0
  172. package/dist/server/upstream/adapter.d.ts +52 -0
  173. package/dist/server/upstream/adapter.d.ts.map +1 -0
  174. package/dist/server/upstream/adapter.js +807 -0
  175. package/dist/server/upstream/adapter.js.map +1 -0
  176. package/dist/server/upstream/credential-resolver.d.ts +14 -0
  177. package/dist/server/upstream/credential-resolver.d.ts.map +1 -0
  178. package/dist/server/upstream/credential-resolver.js +56 -0
  179. package/dist/server/upstream/credential-resolver.js.map +1 -0
  180. package/dist/server/upstream/extension-transport.d.ts +17 -0
  181. package/dist/server/upstream/extension-transport.d.ts.map +1 -0
  182. package/dist/server/upstream/extension-transport.js +207 -0
  183. package/dist/server/upstream/extension-transport.js.map +1 -0
  184. package/dist/server/upstream/manager.d.ts +23 -0
  185. package/dist/server/upstream/manager.d.ts.map +1 -0
  186. package/dist/server/upstream/manager.js +324 -0
  187. package/dist/server/upstream/manager.js.map +1 -0
  188. package/dist/server/upstream/oauth-provider.d.ts +38 -0
  189. package/dist/server/upstream/oauth-provider.d.ts.map +1 -0
  190. package/dist/server/upstream/oauth-provider.js +225 -0
  191. package/dist/server/upstream/oauth-provider.js.map +1 -0
  192. package/dist/server/upstream/oauth-refresh.d.ts +65 -0
  193. package/dist/server/upstream/oauth-refresh.d.ts.map +1 -0
  194. package/dist/server/upstream/oauth-refresh.js +240 -0
  195. package/dist/server/upstream/oauth-refresh.js.map +1 -0
  196. package/dist/server/upstream/oauth-routes.d.ts +17 -0
  197. package/dist/server/upstream/oauth-routes.d.ts.map +1 -0
  198. package/dist/server/upstream/oauth-routes.js +87 -0
  199. package/dist/server/upstream/oauth-routes.js.map +1 -0
  200. package/dist/server/upstream/oauth-service.d.ts +143 -0
  201. package/dist/server/upstream/oauth-service.d.ts.map +1 -0
  202. package/dist/server/upstream/oauth-service.js +217 -0
  203. package/dist/server/upstream/oauth-service.js.map +1 -0
  204. package/dist/server/upstream/stable-json.d.ts +3 -0
  205. package/dist/server/upstream/stable-json.d.ts.map +1 -0
  206. package/dist/server/upstream/stable-json.js +17 -0
  207. package/dist/server/upstream/stable-json.js.map +1 -0
  208. package/package.json +69 -0
  209. package/skills/toolhome/SKILL.md +238 -0
  210. package/skills/toolhome/references/cli-reference.md +242 -0
  211. package/skills/toolhome/references/deployment.md +113 -0
  212. package/skills/toolhome/references/market-guide.md +128 -0
  213. package/skills/toolhome/references/oauth-guide.md +96 -0
  214. package/skills/toolhome/references/troubleshooting.md +133 -0
@@ -0,0 +1,693 @@
1
+ import { z } from 'zod';
2
+ import { AppError } from '../domain/errors.js';
3
+ import { createCredentialInputSchema, createServerInputObjectSchema, createServerInputSchema, parseBucketSeconds, setProjectionInputSchema, toolCallFilterSchema, updateCredentialInputSchema, updateServerInputSchema, } from '../domain/models.js';
4
+ import { parseHarnessConfig, toPreview, } from '../config/harness-import.js';
5
+ const importCredentialSchema = createCredentialInputSchema.extend({
6
+ ref: z.string().min(1).max(200),
7
+ });
8
+ const importServerSchema = createServerInputObjectSchema
9
+ .omit({ credentialId: true })
10
+ .extend({ credentialRef: z.string().min(1).max(200).nullable().default(null) })
11
+ .superRefine((value, context) => {
12
+ const valid = (value.kind === 'remote' && value.transport.type === 'streamable-http') ||
13
+ (value.kind === 'home' && value.transport.type === 'stdio');
14
+ if (!valid) {
15
+ context.addIssue({
16
+ code: 'custom',
17
+ path: ['transport'],
18
+ message: 'Transport does not match server kind',
19
+ });
20
+ }
21
+ });
22
+ const importSchema = z.object({
23
+ version: z.literal(1),
24
+ secretsIncluded: z.literal(true),
25
+ credentials: z.array(importCredentialSchema).default([]),
26
+ servers: z.array(importServerSchema).default([]),
27
+ });
28
+ export class ControlService {
29
+ #store;
30
+ #upstreams;
31
+ #auth;
32
+ #publicUrl;
33
+ #onServerRemoved;
34
+ #onRegistryChanged;
35
+ #upstreamOAuth;
36
+ constructor(store, upstreams, auth, publicUrl, onServerRemoved, onRegistryChanged, upstreamOAuth) {
37
+ this.#store = store;
38
+ this.#upstreams = upstreams;
39
+ this.#auth = auth;
40
+ this.#publicUrl = publicUrl;
41
+ this.#onServerRemoved = onServerRemoved;
42
+ this.#onRegistryChanged = onRegistryChanged;
43
+ this.#upstreamOAuth = upstreamOAuth;
44
+ }
45
+ listServers() {
46
+ return this.#store.listServers().map((server) => ({
47
+ ...server,
48
+ runtime: this.#store.getRuntimeState(server.id),
49
+ }));
50
+ }
51
+ async createServer(value) {
52
+ const input = createServerInputSchema.parse(value);
53
+ this.#assertCredentialAssignment(input.credentialId, input.kind);
54
+ const server = this.#store.createServer(input);
55
+ this.#store.appendEvent({
56
+ level: 'info',
57
+ type: 'server.created',
58
+ serverId: server.id,
59
+ message: `Created ${server.slug}`,
60
+ detail: { kind: server.kind },
61
+ });
62
+ this.#refreshInBackground(server.id);
63
+ return server;
64
+ }
65
+ getServer(id) {
66
+ const server = this.#store.getServer(id);
67
+ if (!server)
68
+ throw new AppError('server_not_found', 'Server not found', 404);
69
+ return { ...server, runtime: this.#store.getRuntimeState(id) };
70
+ }
71
+ async updateServer(id, value) {
72
+ const current = this.getServer(id);
73
+ const input = updateServerInputSchema.parse(value);
74
+ const connectionChanged = input.transport !== undefined ||
75
+ input.credentialId !== undefined ||
76
+ input.settings !== undefined;
77
+ const enabledChanged = input.enabled !== undefined && input.enabled !== current.enabled;
78
+ this.#assertCredentialAssignment(input.credentialId === undefined ? current.credentialId : input.credentialId, current.kind, id);
79
+ const server = this.#store.updateServer(id, input);
80
+ if (connectionChanged)
81
+ await this.#upstreams.invalidate(id);
82
+ else if (!server.enabled)
83
+ await this.#upstreams.remove(id);
84
+ if (connectionChanged || !server.enabled)
85
+ await this.#onServerRemoved(server.slug);
86
+ else if (enabledChanged)
87
+ this.#onRegistryChanged();
88
+ this.#refreshInBackground(server.id);
89
+ return server;
90
+ }
91
+ async deleteServer(id) {
92
+ const server = this.getServer(id);
93
+ await this.#upstreams.remove(id);
94
+ this.#store.deleteServer(id);
95
+ await this.#onServerRemoved(server.slug);
96
+ }
97
+ async enableServer(id) {
98
+ const server = this.#store.updateServer(id, { enabled: true });
99
+ const snapshot = await this.#upstreams.refresh(id);
100
+ this.#onRegistryChanged();
101
+ return { server, snapshot };
102
+ }
103
+ async disableServer(id) {
104
+ const server = this.#store.updateServer(id, { enabled: false });
105
+ await this.#upstreams.remove(id);
106
+ await this.#onServerRemoved(server.slug);
107
+ return server;
108
+ }
109
+ testServer(id) {
110
+ return this.#upstreams.refresh(id);
111
+ }
112
+ refreshServer(id) {
113
+ return this.#upstreams.refresh(id);
114
+ }
115
+ restartServer(id) {
116
+ return this.#upstreams.restart(id);
117
+ }
118
+ serverCapabilities(id) {
119
+ this.getServer(id);
120
+ const snapshot = this.#store.getSnapshot(id);
121
+ if (!snapshot)
122
+ throw new AppError('snapshot_unavailable', 'No capability snapshot', 404);
123
+ return snapshot;
124
+ }
125
+ serverStatus(id) {
126
+ const server = this.getServer(id);
127
+ return {
128
+ server,
129
+ runtime: this.#store.getRuntimeState(id),
130
+ snapshot: this.#store.getSnapshot(id),
131
+ };
132
+ }
133
+ serverLogs(id, limit = 100) {
134
+ this.getServer(id);
135
+ return this.#store.listEvents({ serverId: id, limit });
136
+ }
137
+ serverEndpoint(id) {
138
+ const server = this.getServer(id);
139
+ return {
140
+ aggregateUrl: new URL('/mcp', this.#publicUrl).toString(),
141
+ individualUrl: new URL(`/mcp/${server.slug}`, this.#publicUrl).toString(),
142
+ authorization: { type: 'bearer', credential: 'MCP Access API Key or OAuth access token' },
143
+ };
144
+ }
145
+ aggregateEndpoint() {
146
+ return {
147
+ url: new URL('/mcp', this.#publicUrl).toString(),
148
+ authorization: { type: 'bearer', credential: 'MCP Access API Key or OAuth access token' },
149
+ };
150
+ }
151
+ listCredentials() {
152
+ return this.#store.listCredentials();
153
+ }
154
+ createCredential(value) {
155
+ return this.#store.createCredential(createCredentialInputSchema.parse(value));
156
+ }
157
+ getCredential(id) {
158
+ const credential = this.#store.getCredential(id);
159
+ if (!credential)
160
+ throw new AppError('credential_not_found', 'Credential not found', 404);
161
+ return credential;
162
+ }
163
+ async updateCredential(id, value) {
164
+ const input = updateCredentialInputSchema.parse(value);
165
+ const nextPayload = input.payload ?? this.#store.getCredentialPayload(id);
166
+ if (!nextPayload)
167
+ throw new AppError('credential_not_found', 'Credential not found', 404);
168
+ const attached = this.#store.listServers().filter((server) => server.credentialId === id);
169
+ for (const server of attached)
170
+ this.#assertCredentialPayload(nextPayload, server.kind);
171
+ if (nextPayload.type === 'oauth') {
172
+ if (attached.length > 1) {
173
+ throw new AppError('oauth_credential_reused', 'OAuth credentials cannot be shared between MCP servers', 409);
174
+ }
175
+ }
176
+ const credential = this.#store.updateCredential(id, input);
177
+ await Promise.all(attached.map((server) => this.#upstreams.invalidate(server.id)));
178
+ for (const server of attached)
179
+ this.#refreshInBackground(server.id);
180
+ return credential;
181
+ }
182
+ async deleteCredential(id) {
183
+ const attached = this.#store.listServers().filter((server) => server.credentialId === id);
184
+ for (const server of attached)
185
+ await this.#upstreams.invalidate(server.id);
186
+ this.#store.deleteCredential(id);
187
+ for (const server of attached)
188
+ this.#refreshInBackground(server.id);
189
+ }
190
+ async testCredential(id) {
191
+ const credential = this.getCredential(id);
192
+ const payload = this.#store.getCredentialPayload(id);
193
+ if (!payload)
194
+ throw new AppError('credential_not_found', 'Credential not found', 404);
195
+ const expired = payload.type === 'oauth' && payload.expiresAt
196
+ ? Date.parse(payload.expiresAt) <= Date.now()
197
+ : false;
198
+ const pending = payload.type === 'oauth' && !payload.accessToken;
199
+ const refreshable = payload.type === 'oauth' && payload.refreshToken !== undefined;
200
+ const attached = this.#store.listServers().filter((server) => server.credentialId === id);
201
+ if ((expired && !refreshable) || pending) {
202
+ return {
203
+ credential,
204
+ valid: false,
205
+ requiresAuthorization: true,
206
+ verifiedAgainstUpstream: false,
207
+ servers: attached.map((server) => ({
208
+ id: server.id,
209
+ slug: server.slug,
210
+ ok: false,
211
+ error: pending ? 'Authorization required' : 'Credential expired',
212
+ })),
213
+ };
214
+ }
215
+ const attempts = await Promise.allSettled(attached.map(async (server) => {
216
+ await this.#upstreams.refresh(server.id);
217
+ return { id: server.id, slug: server.slug, ok: true };
218
+ }));
219
+ const servers = attempts.map((attempt, index) => {
220
+ const server = attached[index];
221
+ if (!server)
222
+ throw new Error('Credential verification result mismatch');
223
+ return attempt.status === 'fulfilled'
224
+ ? attempt.value
225
+ : {
226
+ id: server.id,
227
+ slug: server.slug,
228
+ ok: false,
229
+ error: attempt.reason instanceof Error ? attempt.reason.message : String(attempt.reason),
230
+ };
231
+ });
232
+ return {
233
+ credential,
234
+ valid: servers.every((server) => server.ok),
235
+ requiresAuthorization: servers.some((server) => !server.ok &&
236
+ 'error' in server &&
237
+ typeof server.error === 'string' &&
238
+ /401|403|unauthorized|authorization required/i.test(server.error)),
239
+ verifiedAgainstUpstream: attached.length > 0,
240
+ servers,
241
+ };
242
+ }
243
+ authorizeCredential(id, value) {
244
+ this.getCredential(id);
245
+ return this.#upstreamOAuth.begin(id, value);
246
+ }
247
+ revokeCredential(id) {
248
+ this.getCredential(id);
249
+ return this.#upstreamOAuth.revoke(id);
250
+ }
251
+ listKeys(kind) {
252
+ return this.#store.listApiKeys(kind);
253
+ }
254
+ createKey(kind, name, scope) {
255
+ if (kind === 'control' && scope !== undefined) {
256
+ return this.#auth.issue('control', name, scope);
257
+ }
258
+ return this.#auth.issue(kind, name);
259
+ }
260
+ revokeKey(kind, id) {
261
+ const key = this.#store.getApiKey(id, kind);
262
+ if (!key || key.revokedAt !== null) {
263
+ throw new AppError('api_key_not_found', 'API key not found', 404);
264
+ }
265
+ if (kind === 'control' && this.#store.listApiKeys('control').length <= 1) {
266
+ throw new AppError('last_control_key', 'Create another Control API Key before revoking the last active key', 409);
267
+ }
268
+ this.#store.revokeApiKey(id, kind);
269
+ }
270
+ overview() {
271
+ const servers = this.#store.listServers();
272
+ const clis = this.#store.listClis();
273
+ const states = servers.map((server) => this.#store.getRuntimeState(server.id));
274
+ const unhealthy = states.filter((state) => state && !['ready', 'disabled', 'unknown'].includes(state.status)).length;
275
+ return {
276
+ servers: {
277
+ total: servers.length,
278
+ enabled: servers.filter((server) => server.enabled).length,
279
+ remote: servers.filter((server) => server.kind === 'remote').length,
280
+ home: servers.filter((server) => server.kind === 'home').length,
281
+ ready: states.filter((state) => state?.status === 'ready').length,
282
+ unhealthy,
283
+ },
284
+ clis: {
285
+ total: clis.length,
286
+ enabled: clis.filter((cli) => cli.enabled).length,
287
+ },
288
+ credentials: this.#store.listCredentials().length,
289
+ accessKeys: this.#store.listApiKeys('access').length,
290
+ controlKeys: this.#store.listApiKeys('control').length,
291
+ ok: unhealthy === 0,
292
+ endpoints: {
293
+ aggregate: new URL('/mcp', this.#publicUrl).toString(),
294
+ individual: Object.fromEntries(servers.map((server) => [
295
+ server.slug,
296
+ new URL(`/mcp/${server.slug}`, this.#publicUrl).toString(),
297
+ ])),
298
+ },
299
+ };
300
+ }
301
+ diagnostics() {
302
+ const servers = this.#store.listServers();
303
+ const clis = this.#store.listClis();
304
+ return {
305
+ ok: servers.every((server) => {
306
+ const status = this.#store.getRuntimeState(server.id)?.status;
307
+ return !server.enabled || status === 'ready';
308
+ }),
309
+ database: 'ok',
310
+ servers: servers.map((server) => ({
311
+ id: server.id,
312
+ slug: server.slug,
313
+ enabled: server.enabled,
314
+ status: this.#store.getRuntimeState(server.id)?.status ?? 'unknown',
315
+ hasSnapshot: this.#store.getSnapshot(server.id) !== null,
316
+ })),
317
+ clis: clis.map((cli) => ({
318
+ id: cli.id,
319
+ slug: cli.slug,
320
+ enabled: cli.enabled,
321
+ status: cli.enabled ? 'configured' : 'disabled',
322
+ })),
323
+ };
324
+ }
325
+ exportConfig(includeSecrets = false) {
326
+ return {
327
+ version: 1,
328
+ secretsIncluded: includeSecrets,
329
+ exportedAt: new Date().toISOString(),
330
+ credentials: this.#store.listCredentials().map((credential) => {
331
+ const payload = includeSecrets ? this.#store.getCredentialPayload(credential.id) : null;
332
+ return {
333
+ ref: credential.id,
334
+ name: credential.name,
335
+ type: credential.type,
336
+ ...(payload === null ? {} : { payload }),
337
+ };
338
+ }),
339
+ servers: this.#store.listServers().map((server) => ({
340
+ slug: server.slug,
341
+ name: server.name,
342
+ kind: server.kind,
343
+ transport: includeSecrets ? server.transport : redactTransport(server.transport),
344
+ credentialRef: server.credentialId,
345
+ enabled: server.enabled,
346
+ settings: server.settings,
347
+ })),
348
+ };
349
+ }
350
+ importConfig(value) {
351
+ const input = importSchema.parse(value);
352
+ const refs = new Set();
353
+ for (const credential of input.credentials) {
354
+ if (refs.has(credential.ref)) {
355
+ throw new AppError('duplicate_credential_ref', `Duplicate credential ref: ${credential.ref}`, 400);
356
+ }
357
+ refs.add(credential.ref);
358
+ }
359
+ for (const server of input.servers) {
360
+ if (server.credentialRef !== null && !refs.has(server.credentialRef)) {
361
+ throw new AppError('credential_ref_not_found', `Unknown credential ref: ${server.credentialRef}`, 400);
362
+ }
363
+ }
364
+ const imported = this.#store.transaction(() => {
365
+ const credentialIds = new Map();
366
+ const credentials = input.credentials.map((credential) => {
367
+ const created = this.#store.createCredential({
368
+ name: credential.name,
369
+ payload: credential.payload,
370
+ });
371
+ credentialIds.set(credential.ref, created.id);
372
+ return created;
373
+ });
374
+ const servers = input.servers.map((server) => {
375
+ const credentialId = server.credentialRef === null ? null : (credentialIds.get(server.credentialRef) ?? null);
376
+ this.#assertCredentialAssignment(credentialId, server.kind);
377
+ return this.#store.createServer({
378
+ slug: server.slug,
379
+ name: server.name,
380
+ kind: server.kind,
381
+ transport: server.transport,
382
+ credentialId,
383
+ enabled: server.enabled,
384
+ settings: server.settings,
385
+ });
386
+ });
387
+ return { credentials, servers };
388
+ });
389
+ for (const server of imported.servers)
390
+ this.#refreshInBackground(server.id);
391
+ return imported;
392
+ }
393
+ async importHarnessConfig(value, preview = false, mode = 'create') {
394
+ const entries = parseHarnessConfig(value);
395
+ if (preview) {
396
+ return {
397
+ preview: true,
398
+ entries: entries.map((entry) => this.#harnessPreview(entry, mode)),
399
+ };
400
+ }
401
+ const results = await Promise.all(entries.map(async (entry) => {
402
+ const existing = this.#store.getServerBySlug(entry.slug);
403
+ if (existing) {
404
+ if (mode === 'create') {
405
+ return {
406
+ name: entry.name,
407
+ slug: entry.slug,
408
+ status: 'conflict',
409
+ message: `A server with slug "${entry.slug}" already exists`,
410
+ warnings: entry.warnings,
411
+ };
412
+ }
413
+ const changes = this.#harnessDiff(entry, existing);
414
+ if (changes.length === 0) {
415
+ return {
416
+ name: entry.name,
417
+ slug: entry.slug,
418
+ status: 'unchanged',
419
+ warnings: entry.warnings,
420
+ };
421
+ }
422
+ const outcome = this.#applyHarnessUpdate(entry, existing, changes);
423
+ return outcome;
424
+ }
425
+ const created = this.#store.transaction(() => {
426
+ let credentialId = null;
427
+ if (entry.credential !== null) {
428
+ credentialId = this.#store.createCredential({
429
+ name: entry.credential.name,
430
+ payload: entry.credential.payload,
431
+ }).id;
432
+ }
433
+ const server = this.#store.createServer(createServerInputSchema.parse({
434
+ slug: entry.slug,
435
+ name: entry.name,
436
+ kind: entry.kind,
437
+ transport: entry.transport,
438
+ credentialId,
439
+ enabled: true,
440
+ }));
441
+ return { serverId: server.id, credentialId };
442
+ });
443
+ this.#refreshInBackground(created.serverId);
444
+ return {
445
+ name: entry.name,
446
+ slug: entry.slug,
447
+ status: 'created',
448
+ serverId: created.serverId,
449
+ credentialId: created.credentialId,
450
+ warnings: entry.warnings,
451
+ };
452
+ }));
453
+ return { preview: false, entries: results };
454
+ }
455
+ /** Preview with per-entry status (created/changed/unchanged/conflict) — secrets masked. */
456
+ #harnessPreview(entry, mode) {
457
+ const base = toPreview(entry);
458
+ const existing = this.#store.getServerBySlug(entry.slug);
459
+ if (!existing)
460
+ return { ...base, status: 'created', changes: [] };
461
+ if (mode === 'create') {
462
+ return {
463
+ ...base,
464
+ status: 'conflict',
465
+ changes: [],
466
+ message: `A server with slug "${entry.slug}" already exists`,
467
+ };
468
+ }
469
+ const changes = this.#harnessDiff(entry, existing);
470
+ return { ...base, status: changes.length === 0 ? 'unchanged' : 'changed', changes };
471
+ }
472
+ /** Field-level diff between a parsed entry and the stored server — names only, never values. */
473
+ #harnessDiff(entry, existing) {
474
+ const changes = [];
475
+ const current = existing.transport;
476
+ const next = entry.transport;
477
+ if (next.type === 'streamable-http' && current.type === 'streamable-http') {
478
+ if (current.url !== next.url)
479
+ changes.push('url');
480
+ if (JSON.stringify(current.headers) !== JSON.stringify(next.headers)) {
481
+ changes.push('headers');
482
+ }
483
+ }
484
+ else if (next.type === 'stdio' && current.type === 'stdio') {
485
+ if (current.command !== next.command)
486
+ changes.push('command');
487
+ if (JSON.stringify(current.args) !== JSON.stringify(next.args))
488
+ changes.push('args');
489
+ if (JSON.stringify(current.env ?? {}) !== JSON.stringify(next.env ?? {}))
490
+ changes.push('env');
491
+ }
492
+ else if (current.type !== next.type) {
493
+ changes.push('transport');
494
+ }
495
+ const currentPayload = existing.credentialId
496
+ ? this.#store.getCredentialPayload(existing.credentialId)
497
+ : null;
498
+ const nextPayload = entry.credential?.payload ?? null;
499
+ if (JSON.stringify(currentPayload ?? null) !== JSON.stringify(nextPayload)) {
500
+ changes.push('credential');
501
+ }
502
+ return changes;
503
+ }
504
+ #applyHarnessUpdate(entry, existing, changes) {
505
+ return (async () => {
506
+ if (changes.some((change) => change !== 'credential')) {
507
+ await this.updateServer(existing.id, { transport: entry.transport });
508
+ }
509
+ if (changes.includes('credential')) {
510
+ if (existing.credentialId && entry.credential) {
511
+ await this.updateCredential(existing.credentialId, { payload: entry.credential.payload });
512
+ }
513
+ else if (!existing.credentialId && entry.credential) {
514
+ const credential = this.#store.createCredential({
515
+ name: entry.credential.name,
516
+ payload: entry.credential.payload,
517
+ });
518
+ await this.updateServer(existing.id, { credentialId: credential.id });
519
+ }
520
+ // credential removed from the harness config is kept — stripping a
521
+ // credential silently would break a working server; report instead.
522
+ }
523
+ return {
524
+ name: entry.name,
525
+ slug: entry.slug,
526
+ status: 'changed',
527
+ serverId: existing.id,
528
+ changes,
529
+ warnings: entry.warnings,
530
+ };
531
+ })();
532
+ }
533
+ #assertCredentialAssignment(credentialId, serverKind, serverId) {
534
+ if (credentialId === null)
535
+ return;
536
+ const payload = this.#store.getCredentialPayload(credentialId);
537
+ if (!payload)
538
+ throw new AppError('credential_not_found', 'Credential not found', 400);
539
+ this.#assertCredentialPayload(payload, serverKind);
540
+ if (payload.type !== 'oauth')
541
+ return;
542
+ const reused = this.#store
543
+ .listServers()
544
+ .some((server) => server.credentialId === credentialId && server.id !== serverId);
545
+ if (reused) {
546
+ throw new AppError('oauth_credential_reused', 'OAuth credentials cannot be shared between MCP servers', 409);
547
+ }
548
+ }
549
+ #assertCredentialPayload(payload, serverKind) {
550
+ if (serverKind === 'home' && payload.type !== 'env') {
551
+ throw new AppError('credential_kind_mismatch', 'Home-hosted servers only accept environment credentials', 400);
552
+ }
553
+ if (serverKind === 'remote' && payload.type === 'env') {
554
+ throw new AppError('credential_kind_mismatch', 'Remote servers do not accept environment credentials', 400);
555
+ }
556
+ }
557
+ #refreshInBackground(serverId) {
558
+ const server = this.#store.getServer(serverId);
559
+ if (!server?.enabled)
560
+ return;
561
+ void this.#upstreams.refresh(serverId).catch(() => undefined);
562
+ }
563
+ // ── Tool visibility projection ──────────────────────────────────────────
564
+ getProjection(serverId) {
565
+ const server = this.#requireServer(serverId);
566
+ const projection = this.#store.getServerProjection(server.id);
567
+ const overrides = Object.fromEntries(this.#store
568
+ .listToolProjections(server.id)
569
+ .filter((entry) => entry.visibility !== 'inherit')
570
+ .map((entry) => [entry.upstreamToolName, entry.visibility]));
571
+ const snapshot = this.#store.getSnapshot(server.id);
572
+ const tools = (snapshot?.tools ?? []).map((tool) => ({
573
+ name: tool.name,
574
+ description: tool.description ?? '',
575
+ visible: this.#isToolVisible(tool.name, projection?.defaultVisibility, overrides),
576
+ }));
577
+ return {
578
+ serverId: server.id,
579
+ defaultVisibility: projection?.defaultVisibility ?? 'visible',
580
+ overrides,
581
+ tools,
582
+ };
583
+ }
584
+ setProjection(serverId, value) {
585
+ const server = this.#requireServer(serverId);
586
+ const input = setProjectionInputSchema.parse(value);
587
+ if (input.defaultVisibility !== undefined) {
588
+ this.#store.setServerProjection(server.id, input.defaultVisibility);
589
+ }
590
+ for (const override of input.overrides ?? []) {
591
+ this.#store.setToolProjection(server.id, override.tool, override.visibility);
592
+ }
593
+ this.#onRegistryChanged();
594
+ return this.getProjection(server.id);
595
+ }
596
+ #isToolVisible(toolName, defaultVisibility, overrides) {
597
+ const override = overrides[toolName];
598
+ if (override === 'visible' || override === 'hidden')
599
+ return override === 'visible';
600
+ return (defaultVisibility ?? 'visible') === 'visible';
601
+ }
602
+ // ── Tool call observability ─────────────────────────────────────────────
603
+ listCalls(value) {
604
+ const input = {
605
+ limit: value.limit === undefined ? 50 : Number(value.limit),
606
+ offset: value.offset === undefined ? 0 : Number(value.offset),
607
+ serverId: value.server_id,
608
+ tool: value.tool,
609
+ endpointType: value.endpoint_type,
610
+ principalId: value.principal_id,
611
+ status: value.status,
612
+ from: value.from,
613
+ to: value.to,
614
+ };
615
+ const filter = toolCallFilterSchema.parse(input);
616
+ return {
617
+ items: this.#store.listToolCalls(filter),
618
+ total: this.#store.countToolCalls(filter),
619
+ };
620
+ }
621
+ callStats(value) {
622
+ const input = {
623
+ serverId: value.server_id,
624
+ tool: value.tool,
625
+ endpointType: value.endpoint_type,
626
+ from: value.from,
627
+ to: value.to,
628
+ };
629
+ const filter = toolCallFilterSchema.partial().parse({
630
+ limit: 50,
631
+ offset: 0,
632
+ ...input,
633
+ });
634
+ return this.#store.toolCallStats(filter);
635
+ }
636
+ callSeries(value) {
637
+ const bucketSeconds = parseBucketSeconds(typeof value.bucket === 'string' ? value.bucket : undefined);
638
+ const now = new Date();
639
+ const from = typeof value.from === 'string'
640
+ ? value.from
641
+ : new Date(now.getTime() - 24 * 3_600_000).toISOString();
642
+ const to = typeof value.to === 'string' ? value.to : now.toISOString();
643
+ const serverId = typeof value.server_id === 'string' ? value.server_id : undefined;
644
+ const tool = typeof value.tool === 'string' ? value.tool : undefined;
645
+ const endpointType = value.endpoint_type === undefined
646
+ ? undefined
647
+ : z.enum(['aggregate', 'individual', 'management', 'cli']).parse(value.endpoint_type);
648
+ const buckets = this.#store.toolCallSeries({
649
+ from,
650
+ to,
651
+ bucketSeconds,
652
+ serverId,
653
+ tool,
654
+ endpointType,
655
+ });
656
+ const start = Math.floor(Date.parse(from) / 1000 / bucketSeconds);
657
+ const end = Math.floor(Date.parse(to) / 1000 / bucketSeconds);
658
+ const byBucket = new Map(buckets.map((item) => [item.bucket, item]));
659
+ const points = [];
660
+ for (let bucket = start; bucket <= end; bucket += 1) {
661
+ const item = byBucket.get(bucket);
662
+ const total = item?.total ?? 0;
663
+ const success = item?.success ?? 0;
664
+ points.push({
665
+ bucket: new Date(bucket * bucketSeconds * 1000).toISOString(),
666
+ total,
667
+ success,
668
+ error: total - success,
669
+ });
670
+ }
671
+ return { bucketSeconds, points };
672
+ }
673
+ #requireServer(serverId) {
674
+ const server = this.#store.getServer(serverId);
675
+ if (!server) {
676
+ throw new AppError('server_not_found', 'Server not found', 404);
677
+ }
678
+ return server;
679
+ }
680
+ }
681
+ function redactTransport(transport) {
682
+ if (transport.type === 'streamable-http') {
683
+ return {
684
+ ...transport,
685
+ headers: Object.fromEntries(Object.keys(transport.headers).map((name) => [name, '[REDACTED]'])),
686
+ };
687
+ }
688
+ return {
689
+ ...transport,
690
+ env: Object.fromEntries(Object.keys(transport.env).map((name) => [name, '[REDACTED]'])),
691
+ };
692
+ }
693
+ //# sourceMappingURL=control-service.js.map