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,1522 @@
1
+ import { ImplementationSchema, ListPromptsResultSchema, ListResourceTemplatesResultSchema, ListResourcesResultSchema, ListToolsResultSchema, PromptSchema, ResourceSchema, ResourceTemplateSchema, ServerCapabilitiesSchema, ToolSchema, } from '@modelcontextprotocol/core';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { chmodSync, existsSync, renameSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { z } from 'zod';
7
+ import { AppError } from '../domain/errors.js';
8
+ import { assertCliRuntimeConfig, cliRecordSchema, createCliInputSchema, updateCliInputSchema, } from '../cli-plane/models.js';
9
+ import { apiKeyRecordSchema, createServerInputSchema, credentialPayloadSchema, credentialRecordSchema, installJobRecordSchema, marketInstallationSchema, runtimeStateSchema, secureActionRecordSchema, serverProjectionSchema, serverRecordSchema, toolCallSchema, toolProjectionSchema, updateServerInputSchema, } from '../domain/models.js';
10
+ const serverRowSchema = z.object({
11
+ id: z.string(),
12
+ slug: z.string(),
13
+ name: z.string(),
14
+ kind: z.string(),
15
+ transport_json: z.string(),
16
+ credential_id: z.string().nullable(),
17
+ enabled: z.number(),
18
+ settings_json: z.string(),
19
+ created_at: z.string(),
20
+ updated_at: z.string(),
21
+ });
22
+ const cliRowSchema = z.object({
23
+ id: z.string(),
24
+ slug: z.string(),
25
+ name: z.string(),
26
+ command: z.string(),
27
+ execution_mode: z.string(),
28
+ entrypoint: z.string().nullable(),
29
+ auth_strategy: z.string().nullable(),
30
+ container_volumes_json: z.string().nullable(),
31
+ platform: z.string().nullable(),
32
+ allow_list_json: z.string(),
33
+ credential_bindings_json: z.string(),
34
+ interactive: z.number(),
35
+ credential_id: z.string().nullable(),
36
+ probe_json: z.string().nullable(),
37
+ enabled: z.number(),
38
+ timeout_ms: z.number(),
39
+ max_output_bytes: z.number(),
40
+ created_at: z.string(),
41
+ updated_at: z.string(),
42
+ });
43
+ const credentialRowSchema = z.object({
44
+ id: z.string(),
45
+ name: z.string(),
46
+ type: z.string(),
47
+ encrypted_payload: z.string(),
48
+ status: z.string(),
49
+ expires_at: z.string().nullable(),
50
+ created_at: z.string(),
51
+ updated_at: z.string(),
52
+ });
53
+ const apiKeyRowSchema = z.object({
54
+ id: z.string(),
55
+ kind: z.string(),
56
+ name: z.string(),
57
+ prefix: z.string(),
58
+ digest: z.string(),
59
+ scope: z.string().nullable(),
60
+ created_at: z.string(),
61
+ last_used_at: z.string().nullable(),
62
+ revoked_at: z.string().nullable(),
63
+ });
64
+ const snapshotRowSchema = z.object({
65
+ server_id: z.string(),
66
+ version: z.number(),
67
+ protocol_version: z.string(),
68
+ protocol_era: z.string(),
69
+ server_info_json: z.string().nullable(),
70
+ capabilities_json: z.string(),
71
+ instructions: z.string().nullable(),
72
+ tools_json: z.string(),
73
+ resources_json: z.string(),
74
+ resource_templates_json: z.string(),
75
+ prompts_json: z.string(),
76
+ tools_result_json: z.string(),
77
+ resources_result_json: z.string(),
78
+ resource_templates_result_json: z.string(),
79
+ prompts_result_json: z.string(),
80
+ fingerprint: z.string(),
81
+ refreshed_at: z.string(),
82
+ });
83
+ const runtimeRowSchema = z.object({
84
+ server_id: z.string(),
85
+ status: z.string(),
86
+ protocol_version: z.string().nullable(),
87
+ protocol_era: z.string().nullable(),
88
+ process_id: z.number().nullable(),
89
+ restart_count: z.number(),
90
+ last_success_at: z.string().nullable(),
91
+ last_error: z.string().nullable(),
92
+ updated_at: z.string(),
93
+ });
94
+ const eventRowSchema = z.object({
95
+ id: z.string(),
96
+ level: z.string(),
97
+ type: z.string(),
98
+ server_id: z.string().nullable(),
99
+ message: z.string(),
100
+ detail_json: z.string(),
101
+ created_at: z.string(),
102
+ });
103
+ const serverProjectionRowSchema = z.object({
104
+ server_id: z.string(),
105
+ default_visibility: z.string(),
106
+ updated_at: z.string(),
107
+ });
108
+ const toolProjectionRowSchema = z.object({
109
+ server_id: z.string(),
110
+ upstream_tool_name: z.string(),
111
+ visibility: z.string(),
112
+ updated_at: z.string(),
113
+ });
114
+ const toolCallRowSchema = z.object({
115
+ id: z.string(),
116
+ endpoint_type: z.string(),
117
+ principal_kind: z.string(),
118
+ principal_id: z.string(),
119
+ server_id: z.string().nullable(),
120
+ exposed_tool_name: z.string(),
121
+ upstream_tool_name: z.string(),
122
+ status: z.string(),
123
+ error_type: z.string().nullable(),
124
+ started_at: z.string(),
125
+ completed_at: z.string(),
126
+ duration_ms: z.number(),
127
+ });
128
+ const installJobRowSchema = z.object({
129
+ id: z.string(),
130
+ entry_id: z.string(),
131
+ requested_version: z.string().nullable(),
132
+ idempotency_key: z.string(),
133
+ status: z.string(),
134
+ step: z.string(),
135
+ bounded_output: z.string(),
136
+ result_reference: z.string().nullable(),
137
+ action_id: z.string().nullable(),
138
+ error_code: z.string().nullable(),
139
+ created_at: z.string(),
140
+ updated_at: z.string(),
141
+ });
142
+ const installationRowSchema = z.object({
143
+ id: z.string(),
144
+ source: z.string(),
145
+ entry_id: z.string(),
146
+ entry_version: z.string(),
147
+ recipe_revision: z.string(),
148
+ target_type: z.string(),
149
+ target_id: z.string(),
150
+ credential_id: z.string().nullable(),
151
+ installed_at: z.string(),
152
+ });
153
+ const secureActionRowSchema = z.object({
154
+ id: z.string(),
155
+ kind: z.string(),
156
+ target: z.string(),
157
+ principal_id: z.string(),
158
+ status: z.string(),
159
+ values_json: z.string(),
160
+ expires_at: z.string(),
161
+ created_at: z.string(),
162
+ completed_at: z.string().nullable(),
163
+ });
164
+ function parseJson(value) {
165
+ return JSON.parse(value);
166
+ }
167
+ function now() {
168
+ return new Date().toISOString();
169
+ }
170
+ // The 0.4.0 rename moved the default database file from `mcp-home.sqlite` to
171
+ // `toolhome.sqlite`. Move a pre-existing legacy database (and its WAL sidecars)
172
+ // so upgrades keep their data without any manual step.
173
+ function migrateLegacyDatabasePath(databasePath) {
174
+ if (existsSync(databasePath))
175
+ return;
176
+ const legacyBase = basename(databasePath).replace(/^toolhome\.sqlite$/, 'mcp-home.sqlite');
177
+ if (legacyBase === basename(databasePath))
178
+ return;
179
+ const legacyPath = join(dirname(databasePath), legacyBase);
180
+ if (!existsSync(legacyPath))
181
+ return;
182
+ renameSync(legacyPath, databasePath);
183
+ for (const suffix of ['-wal', '-shm']) {
184
+ const legacySidecar = join(dirname(databasePath), `${legacyBase}${suffix}`);
185
+ if (existsSync(legacySidecar)) {
186
+ renameSync(legacySidecar, join(dirname(databasePath), `${basename(databasePath)}${suffix}`));
187
+ }
188
+ }
189
+ }
190
+ const masterKeyCheckSchema = z.object({
191
+ // Accept the pre-0.4.0 'mcp-home-master-key-check' marker written into
192
+ // existing databases so upgrades keep booting with the same master key.
193
+ kind: z.union([z.literal('toolhome-master-key-check'), z.literal('mcp-home-master-key-check')]),
194
+ version: z.literal(1),
195
+ });
196
+ function credentialStatus(payload, timestamp) {
197
+ if (payload.type !== 'oauth')
198
+ return 'ready';
199
+ if (!payload.accessToken)
200
+ return 'pending';
201
+ if (payload.expiresAt && payload.expiresAt <= timestamp)
202
+ return 'expired';
203
+ return 'ready';
204
+ }
205
+ function normalizeCredentialPayload(payload, timestamp) {
206
+ if (payload.type !== 'oauth' ||
207
+ payload.expiresAt !== undefined ||
208
+ payload.expiresIn === undefined) {
209
+ return payload;
210
+ }
211
+ return {
212
+ ...payload,
213
+ expiresAt: new Date(Date.parse(timestamp) + payload.expiresIn * 1_000).toISOString(),
214
+ };
215
+ }
216
+ export class SqliteStore {
217
+ #db;
218
+ #secrets;
219
+ constructor(databasePath, secrets) {
220
+ this.#secrets = secrets;
221
+ migrateLegacyDatabasePath(databasePath);
222
+ this.#db = new DatabaseSync(databasePath);
223
+ chmodSync(databasePath, 0o600);
224
+ this.#db.exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
225
+ this.#migrate();
226
+ this.#verifyCredentialEncryption();
227
+ this.#verifyMasterKey();
228
+ }
229
+ close() {
230
+ this.#db.close();
231
+ }
232
+ transaction(operation) {
233
+ this.#db.exec('BEGIN IMMEDIATE');
234
+ try {
235
+ const value = operation();
236
+ this.#db.exec('COMMIT');
237
+ return value;
238
+ }
239
+ catch (error) {
240
+ this.#db.exec('ROLLBACK');
241
+ throw error;
242
+ }
243
+ }
244
+ #migrate() {
245
+ this.#db.exec(`
246
+ CREATE TABLE IF NOT EXISTS servers (
247
+ id TEXT PRIMARY KEY,
248
+ slug TEXT NOT NULL UNIQUE,
249
+ name TEXT NOT NULL,
250
+ kind TEXT NOT NULL CHECK (kind IN ('remote', 'home')),
251
+ transport_json TEXT NOT NULL,
252
+ credential_id TEXT REFERENCES credentials(id) ON DELETE SET NULL,
253
+ enabled INTEGER NOT NULL,
254
+ settings_json TEXT NOT NULL,
255
+ created_at TEXT NOT NULL,
256
+ updated_at TEXT NOT NULL
257
+ );
258
+
259
+ CREATE TABLE IF NOT EXISTS credentials (
260
+ id TEXT PRIMARY KEY,
261
+ name TEXT NOT NULL,
262
+ type TEXT NOT NULL,
263
+ encrypted_payload TEXT NOT NULL,
264
+ status TEXT NOT NULL,
265
+ expires_at TEXT,
266
+ created_at TEXT NOT NULL,
267
+ updated_at TEXT NOT NULL
268
+ );
269
+
270
+ CREATE TABLE IF NOT EXISTS api_keys (
271
+ id TEXT PRIMARY KEY,
272
+ kind TEXT NOT NULL CHECK (kind IN ('control', 'access')),
273
+ name TEXT NOT NULL,
274
+ prefix TEXT NOT NULL,
275
+ digest TEXT NOT NULL UNIQUE,
276
+ scope TEXT DEFAULT 'admin' CHECK (scope IN ('admin', 'agent')),
277
+ created_at TEXT NOT NULL,
278
+ last_used_at TEXT,
279
+ revoked_at TEXT
280
+ );
281
+
282
+ CREATE TABLE IF NOT EXISTS capability_snapshots (
283
+ server_id TEXT PRIMARY KEY REFERENCES servers(id) ON DELETE CASCADE,
284
+ version INTEGER NOT NULL,
285
+ protocol_version TEXT NOT NULL,
286
+ protocol_era TEXT NOT NULL,
287
+ server_info_json TEXT,
288
+ capabilities_json TEXT NOT NULL,
289
+ instructions TEXT,
290
+ tools_json TEXT NOT NULL,
291
+ resources_json TEXT NOT NULL,
292
+ resource_templates_json TEXT NOT NULL,
293
+ prompts_json TEXT NOT NULL,
294
+ tools_result_json TEXT NOT NULL,
295
+ resources_result_json TEXT NOT NULL,
296
+ resource_templates_result_json TEXT NOT NULL,
297
+ prompts_result_json TEXT NOT NULL,
298
+ fingerprint TEXT NOT NULL,
299
+ refreshed_at TEXT NOT NULL
300
+ );
301
+
302
+ CREATE TABLE IF NOT EXISTS runtime_states (
303
+ server_id TEXT PRIMARY KEY REFERENCES servers(id) ON DELETE CASCADE,
304
+ status TEXT NOT NULL,
305
+ protocol_version TEXT,
306
+ protocol_era TEXT,
307
+ process_id INTEGER,
308
+ restart_count INTEGER NOT NULL,
309
+ last_success_at TEXT,
310
+ last_error TEXT,
311
+ updated_at TEXT NOT NULL
312
+ );
313
+
314
+ CREATE TABLE IF NOT EXISTS events (
315
+ id TEXT PRIMARY KEY,
316
+ level TEXT NOT NULL,
317
+ type TEXT NOT NULL,
318
+ server_id TEXT REFERENCES servers(id) ON DELETE SET NULL,
319
+ message TEXT NOT NULL,
320
+ detail_json TEXT NOT NULL,
321
+ created_at TEXT NOT NULL
322
+ );
323
+
324
+ CREATE TABLE IF NOT EXISTS metadata (
325
+ metadata_key TEXT PRIMARY KEY,
326
+ metadata_value TEXT NOT NULL
327
+ );
328
+
329
+ CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
330
+ CREATE INDEX IF NOT EXISTS idx_events_server_id ON events(server_id, created_at DESC);
331
+
332
+ CREATE TRIGGER IF NOT EXISTS trim_old_events
333
+ AFTER INSERT ON events
334
+ WHEN (SELECT COUNT(*) FROM events) > 10100
335
+ BEGIN
336
+ DELETE FROM events
337
+ WHERE id IN (
338
+ SELECT id FROM events ORDER BY created_at DESC LIMIT -1 OFFSET 10000
339
+ );
340
+ END;
341
+
342
+ CREATE TABLE IF NOT EXISTS server_projections (
343
+ server_id TEXT PRIMARY KEY REFERENCES servers(id) ON DELETE CASCADE,
344
+ default_visibility TEXT NOT NULL CHECK (default_visibility IN ('visible', 'hidden')),
345
+ updated_at TEXT NOT NULL
346
+ );
347
+
348
+ CREATE TABLE IF NOT EXISTS tool_projections (
349
+ server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
350
+ upstream_tool_name TEXT NOT NULL,
351
+ visibility TEXT NOT NULL CHECK (visibility IN ('inherit', 'visible', 'hidden')),
352
+ updated_at TEXT NOT NULL,
353
+ PRIMARY KEY (server_id, upstream_tool_name)
354
+ );
355
+
356
+ CREATE TABLE IF NOT EXISTS tool_calls (
357
+ id TEXT PRIMARY KEY,
358
+ endpoint_type TEXT NOT NULL CHECK (endpoint_type IN ('aggregate', 'individual', 'management', 'cli')),
359
+ principal_kind TEXT NOT NULL,
360
+ principal_id TEXT NOT NULL,
361
+ server_id TEXT REFERENCES servers(id) ON DELETE SET NULL,
362
+ exposed_tool_name TEXT NOT NULL,
363
+ upstream_tool_name TEXT NOT NULL,
364
+ status TEXT NOT NULL,
365
+ error_type TEXT,
366
+ started_at TEXT NOT NULL,
367
+ completed_at TEXT NOT NULL,
368
+ duration_ms INTEGER NOT NULL
369
+ );
370
+
371
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_started ON tool_calls(started_at DESC);
372
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_server ON tool_calls(server_id, started_at DESC);
373
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_principal ON tool_calls(principal_id, started_at DESC);
374
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(upstream_tool_name, started_at DESC);
375
+
376
+ CREATE TRIGGER IF NOT EXISTS trim_tool_calls
377
+ AFTER INSERT ON tool_calls
378
+ WHEN (SELECT COUNT(*) FROM tool_calls) > 200000
379
+ BEGIN
380
+ DELETE FROM tool_calls
381
+ WHERE id IN (
382
+ SELECT id FROM tool_calls ORDER BY started_at DESC LIMIT -1 OFFSET 200000
383
+ );
384
+ END;
385
+
386
+ CREATE TABLE IF NOT EXISTS market_installations (
387
+ id TEXT PRIMARY KEY,
388
+ source TEXT NOT NULL CHECK (source IN ('curated', 'registry')),
389
+ entry_id TEXT NOT NULL,
390
+ entry_version TEXT NOT NULL,
391
+ recipe_revision TEXT NOT NULL,
392
+ target_type TEXT NOT NULL CHECK (target_type IN ('server', 'cli')),
393
+ target_id TEXT NOT NULL,
394
+ credential_id TEXT REFERENCES credentials(id) ON DELETE SET NULL,
395
+ installed_at TEXT NOT NULL
396
+ );
397
+ CREATE INDEX IF NOT EXISTS idx_installations_entry ON market_installations(entry_id);
398
+
399
+ CREATE TABLE IF NOT EXISTS install_jobs (
400
+ id TEXT PRIMARY KEY,
401
+ entry_id TEXT NOT NULL,
402
+ requested_version TEXT,
403
+ idempotency_key TEXT NOT NULL UNIQUE,
404
+ status TEXT NOT NULL CHECK (status IN ('awaiting_secret', 'installing', 'updating', 'completed', 'failed', 'interrupted')),
405
+ step TEXT NOT NULL,
406
+ bounded_output TEXT NOT NULL,
407
+ result_reference TEXT,
408
+ action_id TEXT,
409
+ error_code TEXT,
410
+ created_at TEXT NOT NULL,
411
+ updated_at TEXT NOT NULL
412
+ );
413
+ CREATE INDEX IF NOT EXISTS idx_install_jobs_status ON install_jobs(status, updated_at);
414
+
415
+ CREATE TABLE IF NOT EXISTS secure_actions (
416
+ id TEXT PRIMARY KEY,
417
+ kind TEXT NOT NULL,
418
+ target TEXT NOT NULL,
419
+ principal_id TEXT NOT NULL,
420
+ status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'expired')),
421
+ values_json TEXT NOT NULL DEFAULT '{}',
422
+ expires_at TEXT NOT NULL,
423
+ created_at TEXT NOT NULL,
424
+ completed_at TEXT
425
+ );
426
+ CREATE INDEX IF NOT EXISTS idx_secure_actions_principal ON secure_actions(principal_id);
427
+
428
+ CREATE TABLE IF NOT EXISTS clis (
429
+ id TEXT PRIMARY KEY,
430
+ slug TEXT NOT NULL UNIQUE,
431
+ name TEXT NOT NULL,
432
+ command TEXT NOT NULL,
433
+ execution_mode TEXT NOT NULL CHECK (execution_mode IN ('host', 'docker')),
434
+ entrypoint TEXT,
435
+ auth_strategy TEXT NOT NULL DEFAULT 'none',
436
+ container_volumes_json TEXT NOT NULL DEFAULT '[]',
437
+ platform TEXT,
438
+ allow_list_json TEXT NOT NULL,
439
+ credential_bindings_json TEXT NOT NULL DEFAULT '{}',
440
+ interactive INTEGER NOT NULL,
441
+ credential_id TEXT REFERENCES credentials(id) ON DELETE SET NULL,
442
+ probe_json TEXT,
443
+ enabled INTEGER NOT NULL,
444
+ timeout_ms INTEGER NOT NULL,
445
+ max_output_bytes INTEGER NOT NULL,
446
+ created_at TEXT NOT NULL,
447
+ updated_at TEXT NOT NULL
448
+ );
449
+ CREATE INDEX IF NOT EXISTS idx_clis_slug ON clis(slug);
450
+ `);
451
+ // Guarded migration for existing databases created before api_keys.scope existed.
452
+ const apiKeyColumns = this.#db.prepare('PRAGMA table_info(api_keys)').all();
453
+ if (!apiKeyColumns.some((column) => column.name === 'scope')) {
454
+ this.#db.exec("ALTER TABLE api_keys ADD COLUMN scope TEXT DEFAULT 'admin' CHECK (scope IN ('admin', 'agent'))");
455
+ }
456
+ // Guarded migration for existing CLI records created before Docker entrypoints and
457
+ // platform credential bindings were stored.
458
+ const cliColumns = this.#db.prepare('PRAGMA table_info(clis)').all();
459
+ if (!cliColumns.some((column) => column.name === 'entrypoint')) {
460
+ this.#db.exec('ALTER TABLE clis ADD COLUMN entrypoint TEXT');
461
+ }
462
+ if (!cliColumns.some((column) => column.name === 'auth_strategy')) {
463
+ this.#db.exec("ALTER TABLE clis ADD COLUMN auth_strategy TEXT NOT NULL DEFAULT 'none'");
464
+ }
465
+ if (!cliColumns.some((column) => column.name === 'container_volumes_json')) {
466
+ this.#db.exec("ALTER TABLE clis ADD COLUMN container_volumes_json TEXT NOT NULL DEFAULT '[]'");
467
+ }
468
+ if (!cliColumns.some((column) => column.name === 'platform')) {
469
+ this.#db.exec('ALTER TABLE clis ADD COLUMN platform TEXT');
470
+ }
471
+ if (!cliColumns.some((column) => column.name === 'credential_bindings_json')) {
472
+ this.#db.exec("ALTER TABLE clis ADD COLUMN credential_bindings_json TEXT NOT NULL DEFAULT '{}'");
473
+ }
474
+ // Guarded migration: install_jobs created before the 'updating' status have a
475
+ // CHECK constraint without it. SQLite cannot alter CHECK constraints, so
476
+ // rebuild the table (rows are preserved).
477
+ const installJobsSql = this.#db
478
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'install_jobs'")
479
+ .get();
480
+ if (installJobsSql !== undefined && !installJobsSql.sql.includes("'updating'")) {
481
+ this.#db.exec(`
482
+ BEGIN;
483
+ CREATE TABLE install_jobs_next (
484
+ id TEXT PRIMARY KEY,
485
+ entry_id TEXT NOT NULL,
486
+ requested_version TEXT,
487
+ idempotency_key TEXT NOT NULL UNIQUE,
488
+ status TEXT NOT NULL CHECK (status IN ('awaiting_secret', 'installing', 'updating', 'completed', 'failed', 'interrupted')),
489
+ step TEXT NOT NULL,
490
+ bounded_output TEXT NOT NULL,
491
+ result_reference TEXT,
492
+ action_id TEXT,
493
+ error_code TEXT,
494
+ created_at TEXT NOT NULL,
495
+ updated_at TEXT NOT NULL
496
+ );
497
+ INSERT INTO install_jobs_next
498
+ SELECT id, entry_id, requested_version, idempotency_key, status, step,
499
+ bounded_output, result_reference, action_id, error_code, created_at, updated_at
500
+ FROM install_jobs;
501
+ DROP TABLE install_jobs;
502
+ ALTER TABLE install_jobs_next RENAME TO install_jobs;
503
+ CREATE INDEX IF NOT EXISTS idx_install_jobs_status ON install_jobs(status, updated_at);
504
+ COMMIT;
505
+ `);
506
+ }
507
+ // Guarded migration: tool_calls created before the 'cli' endpoint type have
508
+ // a CHECK constraint without it. SQLite cannot alter CHECK constraints, so
509
+ // rebuild the table (rows are preserved).
510
+ const toolCallsSql = this.#db
511
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'tool_calls'")
512
+ .get();
513
+ if (toolCallsSql !== undefined && !toolCallsSql.sql.includes("'cli'")) {
514
+ this.#db.exec(`
515
+ BEGIN;
516
+ CREATE TABLE tool_calls_next (
517
+ id TEXT PRIMARY KEY,
518
+ endpoint_type TEXT NOT NULL CHECK (endpoint_type IN ('aggregate', 'individual', 'management', 'cli')),
519
+ principal_kind TEXT NOT NULL,
520
+ principal_id TEXT NOT NULL,
521
+ server_id TEXT REFERENCES servers(id) ON DELETE SET NULL,
522
+ exposed_tool_name TEXT NOT NULL,
523
+ upstream_tool_name TEXT NOT NULL,
524
+ status TEXT NOT NULL,
525
+ error_type TEXT,
526
+ started_at TEXT NOT NULL,
527
+ completed_at TEXT NOT NULL,
528
+ duration_ms INTEGER NOT NULL
529
+ );
530
+ INSERT INTO tool_calls_next
531
+ SELECT id, endpoint_type, principal_kind, principal_id, server_id,
532
+ exposed_tool_name, upstream_tool_name, status, error_type,
533
+ started_at, completed_at, duration_ms
534
+ FROM tool_calls;
535
+ DROP TABLE tool_calls;
536
+ ALTER TABLE tool_calls_next RENAME TO tool_calls;
537
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_started ON tool_calls(started_at DESC);
538
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_server ON tool_calls(server_id, started_at DESC);
539
+ COMMIT;
540
+ `);
541
+ }
542
+ // Guarded migration: market installations historically pointed to a server
543
+ // through `server_id`. CLI installs need the same durable record without a
544
+ // foreign key to the servers table, so migrate to a polymorphic target.
545
+ const installationsSql = this.#db
546
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'market_installations'")
547
+ .get();
548
+ if (installationsSql !== undefined && !installationsSql.sql.includes('target_type')) {
549
+ this.#db.exec(`
550
+ BEGIN;
551
+ CREATE TABLE market_installations_next (
552
+ id TEXT PRIMARY KEY,
553
+ source TEXT NOT NULL CHECK (source IN ('curated', 'registry')),
554
+ entry_id TEXT NOT NULL,
555
+ entry_version TEXT NOT NULL,
556
+ recipe_revision TEXT NOT NULL,
557
+ target_type TEXT NOT NULL CHECK (target_type IN ('server', 'cli')),
558
+ target_id TEXT NOT NULL,
559
+ credential_id TEXT REFERENCES credentials(id) ON DELETE SET NULL,
560
+ installed_at TEXT NOT NULL
561
+ );
562
+ INSERT INTO market_installations_next
563
+ SELECT id, source, entry_id, entry_version, recipe_revision,
564
+ 'server', server_id, credential_id, installed_at
565
+ FROM market_installations;
566
+ DROP TABLE market_installations;
567
+ ALTER TABLE market_installations_next RENAME TO market_installations;
568
+ CREATE INDEX IF NOT EXISTS idx_installations_entry ON market_installations(entry_id);
569
+ COMMIT;
570
+ `);
571
+ }
572
+ }
573
+ #verifyCredentialEncryption() {
574
+ const rows = this.#db.prepare('SELECT encrypted_payload FROM credentials').all();
575
+ try {
576
+ for (const row of rows) {
577
+ const payload = z.object({ encrypted_payload: z.string() }).parse(row);
578
+ credentialPayloadSchema.parse(this.#secrets.decrypt(payload.encrypted_payload));
579
+ }
580
+ }
581
+ catch {
582
+ this.#db.close();
583
+ throw new AppError('credential_decryption_failed', 'Stored credentials cannot be decrypted with TOOLHOME_MASTER_KEY', 500);
584
+ }
585
+ }
586
+ #verifyMasterKey() {
587
+ const row = this.#db
588
+ .prepare("SELECT metadata_value FROM metadata WHERE metadata_key = 'master-key-check'")
589
+ .get();
590
+ if (row === undefined) {
591
+ this.#db
592
+ .prepare('INSERT INTO metadata (metadata_key, metadata_value) VALUES (?, ?)')
593
+ .run('master-key-check', this.#secrets.encrypt({ kind: 'toolhome-master-key-check', version: 1 }));
594
+ return;
595
+ }
596
+ try {
597
+ const parsed = z.object({ metadata_value: z.string() }).parse(row);
598
+ masterKeyCheckSchema.parse(this.#secrets.decrypt(parsed.metadata_value));
599
+ }
600
+ catch {
601
+ this.#db.close();
602
+ throw new AppError('master_key_mismatch', 'Stored data cannot be decrypted with TOOLHOME_MASTER_KEY', 500);
603
+ }
604
+ }
605
+ listServers() {
606
+ const rows = this.#db.prepare('SELECT * FROM servers ORDER BY slug').all();
607
+ return rows.map((row) => this.#parseServer(row));
608
+ }
609
+ getServer(id) {
610
+ const row = this.#db.prepare('SELECT * FROM servers WHERE id = ?').get(id);
611
+ return row === undefined ? null : this.#parseServer(row);
612
+ }
613
+ getServerBySlug(slug) {
614
+ const row = this.#db.prepare('SELECT * FROM servers WHERE slug = ?').get(slug);
615
+ return row === undefined ? null : this.#parseServer(row);
616
+ }
617
+ createServer(input) {
618
+ const valid = createServerInputSchema.parse(input);
619
+ if (this.getServerBySlug(valid.slug))
620
+ throw new AppError('slug_conflict', 'Server slug exists', 409);
621
+ const timestamp = now();
622
+ const record = serverRecordSchema.parse({
623
+ id: randomUUID(),
624
+ ...valid,
625
+ createdAt: timestamp,
626
+ updatedAt: timestamp,
627
+ });
628
+ this.#db
629
+ .prepare(`INSERT INTO servers
630
+ (id, slug, name, kind, transport_json, credential_id, enabled, settings_json, created_at, updated_at)
631
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
632
+ .run(record.id, record.slug, record.name, record.kind, JSON.stringify(record.transport), record.credentialId, record.enabled ? 1 : 0, JSON.stringify(record.settings), record.createdAt, record.updatedAt);
633
+ this.saveRuntimeState({
634
+ serverId: record.id,
635
+ status: record.enabled ? 'unknown' : 'disabled',
636
+ protocolVersion: null,
637
+ protocolEra: null,
638
+ processId: null,
639
+ restartCount: 0,
640
+ lastSuccessAt: null,
641
+ lastError: null,
642
+ updatedAt: timestamp,
643
+ });
644
+ return record;
645
+ }
646
+ updateServer(id, input) {
647
+ const current = this.getServer(id);
648
+ if (!current)
649
+ throw new AppError('server_not_found', 'Server not found', 404);
650
+ const patch = updateServerInputSchema.parse(input);
651
+ const record = serverRecordSchema.parse({
652
+ ...current,
653
+ ...patch,
654
+ settings: { ...current.settings, ...patch.settings },
655
+ updatedAt: now(),
656
+ });
657
+ const expectedTransport = record.kind === 'remote' ? 'streamable-http' : 'stdio';
658
+ if (record.transport.type !== expectedTransport) {
659
+ throw new AppError('invalid_transport', 'Transport does not match server kind');
660
+ }
661
+ this.#db
662
+ .prepare(`UPDATE servers SET name = ?, transport_json = ?, credential_id = ?, enabled = ?,
663
+ settings_json = ?, updated_at = ? WHERE id = ?`)
664
+ .run(record.name, JSON.stringify(record.transport), record.credentialId, record.enabled ? 1 : 0, JSON.stringify(record.settings), record.updatedAt, id);
665
+ if (!record.enabled) {
666
+ const state = this.getRuntimeState(id);
667
+ this.saveRuntimeState({
668
+ serverId: id,
669
+ status: 'disabled',
670
+ protocolVersion: state?.protocolVersion ?? null,
671
+ protocolEra: state?.protocolEra ?? null,
672
+ processId: null,
673
+ restartCount: state?.restartCount ?? 0,
674
+ lastSuccessAt: state?.lastSuccessAt ?? null,
675
+ lastError: null,
676
+ updatedAt: now(),
677
+ });
678
+ }
679
+ return record;
680
+ }
681
+ deleteServer(id) {
682
+ const result = this.#db.prepare('DELETE FROM servers WHERE id = ?').run(id);
683
+ if (result.changes === 0)
684
+ throw new AppError('server_not_found', 'Server not found', 404);
685
+ }
686
+ listClis() {
687
+ return this.#db
688
+ .prepare('SELECT * FROM clis ORDER BY slug')
689
+ .all()
690
+ .map((row) => this.#parseCli(row));
691
+ }
692
+ getCli(id) {
693
+ const row = this.#db.prepare('SELECT * FROM clis WHERE id = ?').get(id);
694
+ return row === undefined ? null : this.#parseCli(row);
695
+ }
696
+ getCliBySlug(slug) {
697
+ const row = this.#db.prepare('SELECT * FROM clis WHERE slug = ?').get(slug);
698
+ return row === undefined ? null : this.#parseCli(row);
699
+ }
700
+ createCli(input) {
701
+ const valid = createCliInputSchema.parse(input);
702
+ if (this.getCliBySlug(valid.slug)) {
703
+ throw new AppError('cli_slug_conflict', 'CLI slug exists', 409);
704
+ }
705
+ const timestamp = now();
706
+ assertCliRuntimeConfig(valid);
707
+ const record = cliRecordSchema.parse({
708
+ id: randomUUID(),
709
+ ...valid,
710
+ createdAt: timestamp,
711
+ updatedAt: timestamp,
712
+ });
713
+ this.#db
714
+ .prepare(`INSERT INTO clis
715
+ (id, slug, name, command, execution_mode, entrypoint, auth_strategy, container_volumes_json,
716
+ platform, allow_list_json, credential_bindings_json, interactive, credential_id, probe_json,
717
+ enabled, timeout_ms, max_output_bytes, created_at, updated_at)
718
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
719
+ .run(record.id, record.slug, record.name, record.command, record.executionMode, record.entrypoint, record.authStrategy, JSON.stringify(record.containerVolumes), record.platform, JSON.stringify(record.allowList), JSON.stringify(record.credentialBindings), record.interactive ? 1 : 0, record.credentialId, record.probe === null ? null : JSON.stringify(record.probe), record.enabled ? 1 : 0, record.timeoutMs, record.maxOutputBytes, record.createdAt, record.updatedAt);
720
+ return record;
721
+ }
722
+ updateCli(id, input) {
723
+ const current = this.getCli(id);
724
+ if (!current)
725
+ throw new AppError('cli_not_found', 'CLI not found', 404);
726
+ const patch = updateCliInputSchema.parse(input);
727
+ const record = cliRecordSchema.parse({
728
+ ...current,
729
+ ...patch,
730
+ updatedAt: now(),
731
+ });
732
+ assertCliRuntimeConfig(record);
733
+ this.#db
734
+ .prepare(`UPDATE clis SET name = ?, command = ?, execution_mode = ?, entrypoint = ?, auth_strategy = ?,
735
+ container_volumes_json = ?, platform = ?, allow_list_json = ?, credential_bindings_json = ?,
736
+ interactive = ?, credential_id = ?, probe_json = ?, enabled = ?, timeout_ms = ?,
737
+ max_output_bytes = ?, updated_at = ? WHERE id = ?`)
738
+ .run(record.name, record.command, record.executionMode, record.entrypoint, record.authStrategy, JSON.stringify(record.containerVolumes), record.platform, JSON.stringify(record.allowList), JSON.stringify(record.credentialBindings), record.interactive ? 1 : 0, record.credentialId, record.probe === null ? null : JSON.stringify(record.probe), record.enabled ? 1 : 0, record.timeoutMs, record.maxOutputBytes, record.updatedAt, id);
739
+ return record;
740
+ }
741
+ deleteCli(id) {
742
+ const result = this.#db.prepare('DELETE FROM clis WHERE id = ?').run(id);
743
+ if (result.changes === 0)
744
+ throw new AppError('cli_not_found', 'CLI not found', 404);
745
+ }
746
+ listCredentials() {
747
+ return this.#db
748
+ .prepare('SELECT * FROM credentials ORDER BY name')
749
+ .all()
750
+ .map((row) => this.#parseCredential(row));
751
+ }
752
+ getCredential(id) {
753
+ const row = this.#db.prepare('SELECT * FROM credentials WHERE id = ?').get(id);
754
+ return row === undefined ? null : this.#parseCredential(row);
755
+ }
756
+ getCredentialPayload(id) {
757
+ const row = this.#db.prepare('SELECT encrypted_payload FROM credentials WHERE id = ?').get(id);
758
+ if (row === undefined)
759
+ return null;
760
+ const parsed = z.object({ encrypted_payload: z.string() }).parse(row);
761
+ return credentialPayloadSchema.parse(this.#secrets.decrypt(parsed.encrypted_payload));
762
+ }
763
+ createCredential(input) {
764
+ const timestamp = now();
765
+ const payload = normalizeCredentialPayload(input.payload, timestamp);
766
+ const expiresAt = payload.type === 'oauth' ? (payload.expiresAt ?? null) : null;
767
+ const record = credentialRecordSchema.parse({
768
+ id: randomUUID(),
769
+ name: input.name,
770
+ type: payload.type,
771
+ status: credentialStatus(payload, timestamp),
772
+ expiresAt,
773
+ createdAt: timestamp,
774
+ updatedAt: timestamp,
775
+ });
776
+ this.#db
777
+ .prepare(`INSERT INTO credentials
778
+ (id, name, type, encrypted_payload, status, expires_at, created_at, updated_at)
779
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
780
+ .run(record.id, record.name, record.type, this.#secrets.encrypt(payload), record.status, record.expiresAt, record.createdAt, record.updatedAt);
781
+ return record;
782
+ }
783
+ updateCredential(id, input) {
784
+ const current = this.getCredential(id);
785
+ if (!current)
786
+ throw new AppError('credential_not_found', 'Credential not found', 404);
787
+ const currentPayload = input.payload ?? this.getCredentialPayload(id);
788
+ if (!currentPayload)
789
+ throw new AppError('credential_not_found', 'Credential not found', 404);
790
+ const timestamp = now();
791
+ const payload = normalizeCredentialPayload(currentPayload, timestamp);
792
+ const expiresAt = payload.type === 'oauth' ? (payload.expiresAt ?? null) : null;
793
+ const record = credentialRecordSchema.parse({
794
+ ...current,
795
+ name: input.name ?? current.name,
796
+ type: payload.type,
797
+ status: credentialStatus(payload, timestamp),
798
+ expiresAt,
799
+ updatedAt: timestamp,
800
+ });
801
+ this.#db
802
+ .prepare(`UPDATE credentials SET name = ?, type = ?, encrypted_payload = ?, status = ?,
803
+ expires_at = ?, updated_at = ? WHERE id = ?`)
804
+ .run(record.name, record.type, this.#secrets.encrypt(payload), record.status, record.expiresAt, record.updatedAt, id);
805
+ return record;
806
+ }
807
+ deleteCredential(id) {
808
+ const result = this.#db.prepare('DELETE FROM credentials WHERE id = ?').run(id);
809
+ if (result.changes === 0)
810
+ throw new AppError('credential_not_found', 'Credential not found', 404);
811
+ }
812
+ listApiKeys(kind) {
813
+ return this.#db
814
+ .prepare('SELECT * FROM api_keys WHERE kind = ? AND revoked_at IS NULL ORDER BY created_at DESC')
815
+ .all(kind)
816
+ .map((row) => this.#parseApiKey(row));
817
+ }
818
+ createApiKey(input) {
819
+ const timestamp = now();
820
+ const id = randomUUID();
821
+ this.#db
822
+ .prepare(`INSERT INTO api_keys
823
+ (id, kind, name, prefix, digest, scope, created_at, last_used_at, revoked_at)
824
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL)`)
825
+ .run(id, input.kind, input.name, input.prefix, input.digest, input.scope ?? null, timestamp);
826
+ const key = this.getApiKeyByDigest(input.kind, input.digest);
827
+ if (!key)
828
+ throw new Error('API key insert failed');
829
+ const { digest: _digest, ...record } = key;
830
+ return record;
831
+ }
832
+ getApiKey(id, kind) {
833
+ const row = this.#db.prepare('SELECT * FROM api_keys WHERE id = ? AND kind = ?').get(id, kind);
834
+ return row === undefined ? null : this.#parseApiKey(row);
835
+ }
836
+ getApiKeyByDigest(kind, digest) {
837
+ const row = this.#db
838
+ .prepare('SELECT * FROM api_keys WHERE kind = ? AND digest = ? AND revoked_at IS NULL')
839
+ .get(kind, digest);
840
+ if (row === undefined)
841
+ return null;
842
+ const parsed = apiKeyRowSchema.parse(row);
843
+ return {
844
+ ...this.#parseApiKey(parsed),
845
+ digest: parsed.digest,
846
+ };
847
+ }
848
+ revokeApiKey(id, kind) {
849
+ const result = this.#db
850
+ .prepare('UPDATE api_keys SET revoked_at = ? WHERE id = ? AND kind = ? AND revoked_at IS NULL')
851
+ .run(now(), id, kind);
852
+ if (result.changes === 0)
853
+ throw new AppError('api_key_not_found', 'API key not found', 404);
854
+ }
855
+ touchApiKey(id) {
856
+ this.#db.prepare('UPDATE api_keys SET last_used_at = ? WHERE id = ?').run(now(), id);
857
+ }
858
+ getSnapshot(serverId) {
859
+ const row = this.#db
860
+ .prepare('SELECT * FROM capability_snapshots WHERE server_id = ?')
861
+ .get(serverId);
862
+ return row === undefined ? null : this.#parseSnapshot(row);
863
+ }
864
+ saveSnapshot(snapshot) {
865
+ this.#db
866
+ .prepare(`INSERT INTO capability_snapshots
867
+ (server_id, version, protocol_version, protocol_era, server_info_json, capabilities_json,
868
+ instructions, tools_json, resources_json, resource_templates_json, prompts_json,
869
+ tools_result_json, resources_result_json, resource_templates_result_json,
870
+ prompts_result_json, fingerprint, refreshed_at)
871
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
872
+ ON CONFLICT(server_id) DO UPDATE SET
873
+ version = excluded.version,
874
+ protocol_version = excluded.protocol_version,
875
+ protocol_era = excluded.protocol_era,
876
+ server_info_json = excluded.server_info_json,
877
+ capabilities_json = excluded.capabilities_json,
878
+ instructions = excluded.instructions,
879
+ tools_json = excluded.tools_json,
880
+ resources_json = excluded.resources_json,
881
+ resource_templates_json = excluded.resource_templates_json,
882
+ prompts_json = excluded.prompts_json,
883
+ tools_result_json = excluded.tools_result_json,
884
+ resources_result_json = excluded.resources_result_json,
885
+ resource_templates_result_json = excluded.resource_templates_result_json,
886
+ prompts_result_json = excluded.prompts_result_json,
887
+ fingerprint = excluded.fingerprint,
888
+ refreshed_at = excluded.refreshed_at`)
889
+ .run(snapshot.serverId, snapshot.version, snapshot.protocolVersion, snapshot.protocolEra, snapshot.serverInfo === null ? null : JSON.stringify(snapshot.serverInfo), JSON.stringify(snapshot.capabilities), snapshot.instructions, JSON.stringify(snapshot.tools), JSON.stringify(snapshot.resources), JSON.stringify(snapshot.resourceTemplates), JSON.stringify(snapshot.prompts), JSON.stringify(snapshot.listResults.tools), JSON.stringify(snapshot.listResults.resources), JSON.stringify(snapshot.listResults.resourceTemplates), JSON.stringify(snapshot.listResults.prompts), snapshot.fingerprint, snapshot.refreshedAt);
890
+ const saved = this.getSnapshot(snapshot.serverId);
891
+ if (!saved)
892
+ throw new Error('Snapshot insert failed');
893
+ return saved;
894
+ }
895
+ deleteSnapshot(serverId) {
896
+ this.#db.prepare('DELETE FROM capability_snapshots WHERE server_id = ?').run(serverId);
897
+ }
898
+ getRuntimeState(serverId) {
899
+ const row = this.#db.prepare('SELECT * FROM runtime_states WHERE server_id = ?').get(serverId);
900
+ return row === undefined ? null : this.#parseRuntimeState(row);
901
+ }
902
+ saveRuntimeState(state) {
903
+ const valid = runtimeStateSchema.parse(state);
904
+ this.#db
905
+ .prepare(`INSERT INTO runtime_states
906
+ (server_id, status, protocol_version, protocol_era, process_id, restart_count,
907
+ last_success_at, last_error, updated_at)
908
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
909
+ ON CONFLICT(server_id) DO UPDATE SET
910
+ status = excluded.status,
911
+ protocol_version = excluded.protocol_version,
912
+ protocol_era = excluded.protocol_era,
913
+ process_id = excluded.process_id,
914
+ restart_count = excluded.restart_count,
915
+ last_success_at = excluded.last_success_at,
916
+ last_error = excluded.last_error,
917
+ updated_at = excluded.updated_at`)
918
+ .run(valid.serverId, valid.status, valid.protocolVersion, valid.protocolEra, valid.processId, valid.restartCount, valid.lastSuccessAt, valid.lastError, valid.updatedAt);
919
+ return valid;
920
+ }
921
+ appendEvent(event) {
922
+ const record = { ...event, id: randomUUID(), createdAt: now() };
923
+ this.#db
924
+ .prepare(`INSERT INTO events (id, level, type, server_id, message, detail_json, created_at)
925
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
926
+ .run(record.id, record.level, record.type, record.serverId, record.message, JSON.stringify(record.detail), record.createdAt);
927
+ return record;
928
+ }
929
+ listEvents(options = {}) {
930
+ const limit = Math.max(1, Math.min(options.limit ?? 100, 1_000));
931
+ const conditions = [];
932
+ const params = [];
933
+ if (options.serverId !== undefined) {
934
+ conditions.push('server_id = ?');
935
+ params.push(options.serverId);
936
+ }
937
+ if (options.level !== undefined) {
938
+ conditions.push('level = ?');
939
+ params.push(options.level);
940
+ }
941
+ if (options.plane === 'cli')
942
+ conditions.push("type LIKE 'cli.%'");
943
+ if (options.plane === 'mcp')
944
+ conditions.push("type NOT LIKE 'cli.%'");
945
+ const where = conditions.length === 0 ? '' : `WHERE ${conditions.join(' AND ')}`;
946
+ const rows = this.#db
947
+ .prepare(`SELECT * FROM events ${where} ORDER BY created_at DESC LIMIT ?`)
948
+ .all(...params, limit);
949
+ return rows.map((row) => this.#parseEvent(row));
950
+ }
951
+ // ── Tool visibility projection ──────────────────────────────────────────
952
+ getServerProjection(serverId) {
953
+ const row = this.#db
954
+ .prepare('SELECT * FROM server_projections WHERE server_id = ?')
955
+ .get(serverId);
956
+ return row === undefined ? null : this.#parseServerProjection(row);
957
+ }
958
+ setServerProjection(serverId, defaultVisibility) {
959
+ const timestamp = now();
960
+ this.#db
961
+ .prepare(`INSERT INTO server_projections (server_id, default_visibility, updated_at)
962
+ VALUES (?, ?, ?)
963
+ ON CONFLICT(server_id) DO UPDATE SET
964
+ default_visibility = excluded.default_visibility,
965
+ updated_at = excluded.updated_at`)
966
+ .run(serverId, defaultVisibility, timestamp);
967
+ return serverProjectionSchema.parse({
968
+ serverId,
969
+ defaultVisibility,
970
+ updatedAt: timestamp,
971
+ });
972
+ }
973
+ listToolProjections(serverId) {
974
+ const rows = serverId
975
+ ? this.#db
976
+ .prepare('SELECT * FROM tool_projections WHERE server_id = ? ORDER BY upstream_tool_name')
977
+ .all(serverId)
978
+ : this.#db
979
+ .prepare('SELECT * FROM tool_projections ORDER BY server_id, upstream_tool_name')
980
+ .all();
981
+ return rows.map((row) => this.#parseToolProjection(row));
982
+ }
983
+ setToolProjection(serverId, toolName, visibility) {
984
+ if (visibility === 'inherit') {
985
+ this.#db
986
+ .prepare('DELETE FROM tool_projections WHERE server_id = ? AND upstream_tool_name = ?')
987
+ .run(serverId, toolName);
988
+ return;
989
+ }
990
+ this.#db
991
+ .prepare(`INSERT INTO tool_projections (server_id, upstream_tool_name, visibility, updated_at)
992
+ VALUES (?, ?, ?, ?)
993
+ ON CONFLICT(server_id, upstream_tool_name) DO UPDATE SET
994
+ visibility = excluded.visibility,
995
+ updated_at = excluded.updated_at`)
996
+ .run(serverId, toolName, visibility, now());
997
+ }
998
+ getProjectionIndex() {
999
+ const index = new Map();
1000
+ for (const row of this.#db.prepare('SELECT * FROM server_projections').all()) {
1001
+ const parsed = this.#parseServerProjection(row);
1002
+ index.set(parsed.serverId, {
1003
+ defaultVisibility: parsed.defaultVisibility,
1004
+ overrides: new Map(),
1005
+ });
1006
+ }
1007
+ for (const row of this.#db.prepare('SELECT * FROM tool_projections').all()) {
1008
+ const parsed = this.#parseToolProjection(row);
1009
+ if (parsed.visibility === 'inherit')
1010
+ continue;
1011
+ const entry = index.get(parsed.serverId) ?? {
1012
+ defaultVisibility: 'visible',
1013
+ overrides: new Map(),
1014
+ };
1015
+ entry.overrides.set(parsed.upstreamToolName, parsed.visibility);
1016
+ index.set(parsed.serverId, entry);
1017
+ }
1018
+ return index;
1019
+ }
1020
+ // ── Tool call observability ─────────────────────────────────────────────
1021
+ insertToolCalls(calls) {
1022
+ if (calls.length === 0)
1023
+ return 0;
1024
+ const statement = this.#db.prepare(`INSERT INTO tool_calls (
1025
+ id, endpoint_type, principal_kind, principal_id, server_id,
1026
+ exposed_tool_name, upstream_tool_name, status, error_type,
1027
+ started_at, completed_at, duration_ms
1028
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1029
+ for (const call of calls) {
1030
+ const record = toolCallSchema.parse({ id: randomUUID(), ...call });
1031
+ statement.run(record.id, record.endpointType, record.principalKind, record.principalId, record.serverId, record.exposedToolName, record.upstreamToolName, record.status, record.errorType, record.startedAt, record.completedAt, record.durationMs);
1032
+ }
1033
+ return calls.length;
1034
+ }
1035
+ listToolCalls(filter) {
1036
+ const { where, params } = this.#callWhere(filter);
1037
+ const rows = this.#db
1038
+ .prepare(`SELECT * FROM tool_calls
1039
+ ${where}
1040
+ ORDER BY started_at DESC, id DESC
1041
+ LIMIT ? OFFSET ?`)
1042
+ .all(...params, filter.limit, filter.offset);
1043
+ return rows.map((row) => this.#parseToolCall(row));
1044
+ }
1045
+ countToolCalls(filter) {
1046
+ const { where, params } = this.#callWhere(filter);
1047
+ const row = this.#db.prepare(`SELECT COUNT(*) AS n FROM tool_calls ${where}`).get(...params);
1048
+ return row.n;
1049
+ }
1050
+ toolCallStats(filter) {
1051
+ const { where, params } = this.#callWhere({ ...filter, limit: 50, offset: 0 });
1052
+ const totalRow = this.#db
1053
+ .prepare(`SELECT COUNT(*) AS n FROM tool_calls ${where}`)
1054
+ .get(...params);
1055
+ const total = totalRow.n;
1056
+ const byStatus = {};
1057
+ for (const row of this.#db
1058
+ .prepare(`SELECT status, COUNT(*) AS n FROM tool_calls ${where} GROUP BY status`)
1059
+ .all(...params)) {
1060
+ const parsed = row;
1061
+ byStatus[parsed.status] = parsed.n;
1062
+ }
1063
+ const durationRows = this.#db
1064
+ .prepare(`SELECT duration_ms FROM tool_calls
1065
+ ${where}
1066
+ ORDER BY started_at DESC LIMIT 10000`)
1067
+ .all(...params);
1068
+ const durations = durationRows.map((row) => row.duration_ms).sort((a, b) => a - b);
1069
+ const avgDurationMs = durations.length === 0
1070
+ ? 0
1071
+ : Math.round(durations.reduce((sum, value) => sum + value, 0) / durations.length);
1072
+ const percentile = (p) => {
1073
+ if (durations.length === 0)
1074
+ return 0;
1075
+ const index = Math.min(durations.length - 1, Math.ceil((p / 100) * durations.length) - 1);
1076
+ return durations[index] ?? 0;
1077
+ };
1078
+ const topTools = this.#db
1079
+ .prepare(`SELECT upstream_tool_name AS tool, COUNT(*) AS n FROM tool_calls
1080
+ ${where}
1081
+ GROUP BY upstream_tool_name ORDER BY n DESC LIMIT 10`)
1082
+ .all(...params).map((row) => ({ tool: row.tool, count: row.n }));
1083
+ const failingWhere = where === '' ? 'WHERE status != ?' : `${where} AND status != ?`;
1084
+ const failingParams = [...params, 'success'];
1085
+ const topFailing = this.#db
1086
+ .prepare(`SELECT upstream_tool_name AS tool, error_type AS errorType, COUNT(*) AS n FROM tool_calls
1087
+ ${failingWhere}
1088
+ GROUP BY upstream_tool_name, error_type ORDER BY n DESC LIMIT 10`)
1089
+ .all(...failingParams).map((row) => ({ tool: row.tool, errorType: row.errorType, count: row.n }));
1090
+ const success = byStatus.success ?? 0;
1091
+ const error = total - success;
1092
+ return {
1093
+ total,
1094
+ byStatus,
1095
+ success,
1096
+ error,
1097
+ successRate: total === 0 ? 0 : Math.round((success / total) * 1000) / 10,
1098
+ avgDurationMs,
1099
+ p50Ms: percentile(50),
1100
+ p95Ms: percentile(95),
1101
+ topTools,
1102
+ topFailing,
1103
+ };
1104
+ }
1105
+ toolCallSeries(query) {
1106
+ const conditions = [];
1107
+ const params = [query.bucketSeconds];
1108
+ if (query.serverId !== undefined) {
1109
+ conditions.push('server_id = ?');
1110
+ params.push(query.serverId);
1111
+ }
1112
+ if (query.tool !== undefined) {
1113
+ conditions.push('upstream_tool_name = ?');
1114
+ params.push(query.tool);
1115
+ }
1116
+ if (query.endpointType !== undefined) {
1117
+ conditions.push('endpoint_type = ?');
1118
+ params.push(query.endpointType);
1119
+ }
1120
+ if (query.from !== undefined) {
1121
+ conditions.push('started_at >= ?');
1122
+ params.push(query.from);
1123
+ }
1124
+ if (query.to !== undefined) {
1125
+ conditions.push('started_at <= ?');
1126
+ params.push(query.to);
1127
+ }
1128
+ const where = conditions.length === 0 ? '' : `WHERE ${conditions.join(' AND ')}`;
1129
+ const rows = this.#db
1130
+ .prepare(`SELECT CAST(CAST(strftime('%s', started_at) AS INTEGER) / ? AS INTEGER) AS bucket,
1131
+ COUNT(*) AS total,
1132
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success
1133
+ FROM tool_calls
1134
+ ${where}
1135
+ GROUP BY bucket
1136
+ ORDER BY bucket ASC`)
1137
+ .all(...params);
1138
+ return rows.map((row) => ({
1139
+ bucket: Number(row.bucket),
1140
+ total: Number(row.total),
1141
+ success: Number(row.success),
1142
+ }));
1143
+ }
1144
+ deleteOldToolCalls(before) {
1145
+ let total = 0;
1146
+ for (;;) {
1147
+ const rows = this.#db
1148
+ .prepare('SELECT id FROM tool_calls WHERE started_at < ? ORDER BY started_at ASC LIMIT 500')
1149
+ .all(before);
1150
+ if (rows.length === 0)
1151
+ break;
1152
+ const statement = this.#db.prepare('DELETE FROM tool_calls WHERE id = ?');
1153
+ for (const row of rows)
1154
+ statement.run(row.id);
1155
+ total += rows.length;
1156
+ }
1157
+ return total;
1158
+ }
1159
+ // ── Market install records & persistent jobs ───────────────────────────
1160
+ listInstallations() {
1161
+ return this.#db
1162
+ .prepare('SELECT * FROM market_installations ORDER BY installed_at DESC')
1163
+ .all()
1164
+ .map((row) => this.#parseInstallation(row));
1165
+ }
1166
+ getInstallation(entryId) {
1167
+ const row = this.#db
1168
+ .prepare('SELECT * FROM market_installations WHERE entry_id = ? ORDER BY installed_at DESC LIMIT 1')
1169
+ .get(entryId);
1170
+ return row === undefined ? null : this.#parseInstallation(row);
1171
+ }
1172
+ createInstallation(input) {
1173
+ const record = marketInstallationSchema.parse({
1174
+ id: randomUUID(),
1175
+ ...input,
1176
+ installedAt: now(),
1177
+ });
1178
+ this.#db
1179
+ .prepare(`INSERT INTO market_installations
1180
+ (id, source, entry_id, entry_version, recipe_revision, target_type, target_id, credential_id, installed_at)
1181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1182
+ .run(record.id, record.source, record.entryId, record.entryVersion, record.recipeRevision, record.targetType, record.targetId, record.credentialId, record.installedAt);
1183
+ return record;
1184
+ }
1185
+ updateInstallation(id, patch) {
1186
+ this.#db
1187
+ .prepare(`UPDATE market_installations
1188
+ SET entry_version = COALESCE(?, entry_version),
1189
+ recipe_revision = COALESCE(?, recipe_revision)
1190
+ WHERE id = ?`)
1191
+ .run(patch.entryVersion ?? null, patch.recipeRevision ?? null, id);
1192
+ const row = this.#db.prepare('SELECT * FROM market_installations WHERE id = ?').get(id);
1193
+ if (row === undefined)
1194
+ throw new AppError('market_installation_not_found', 'Installation not found', 404);
1195
+ return this.#parseInstallation(row);
1196
+ }
1197
+ deleteInstallation(id) {
1198
+ this.#db.prepare('DELETE FROM market_installations WHERE id = ?').run(id);
1199
+ }
1200
+ createInstallJob(input) {
1201
+ const timestamp = now();
1202
+ const record = installJobRecordSchema.parse({
1203
+ id: randomUUID(),
1204
+ ...input,
1205
+ createdAt: timestamp,
1206
+ updatedAt: timestamp,
1207
+ });
1208
+ this.#db
1209
+ .prepare(`INSERT INTO install_jobs
1210
+ (id, entry_id, requested_version, idempotency_key, status, step, bounded_output,
1211
+ result_reference, action_id, error_code, created_at, updated_at)
1212
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1213
+ .run(record.id, record.entryId, record.requestedVersion, record.idempotencyKey, record.status, record.step, record.boundedOutput, record.resultReference, record.actionId, record.errorCode, record.createdAt, record.updatedAt);
1214
+ return record;
1215
+ }
1216
+ getInstallJob(id) {
1217
+ const row = this.#db.prepare('SELECT * FROM install_jobs WHERE id = ?').get(id);
1218
+ return row === undefined ? null : this.#parseInstallJob(row);
1219
+ }
1220
+ updateInstallJob(id, patch) {
1221
+ const current = this.#db.prepare('SELECT * FROM install_jobs WHERE id = ?').get(id);
1222
+ if (current === undefined) {
1223
+ throw new AppError('market_job_not_found', 'Install job not found', 404);
1224
+ }
1225
+ const merged = installJobRecordSchema.parse({
1226
+ ...this.#parseInstallJob(current),
1227
+ ...patch,
1228
+ updatedAt: now(),
1229
+ });
1230
+ this.#db
1231
+ .prepare(`UPDATE install_jobs SET
1232
+ requested_version = ?, idempotency_key = ?, status = ?, step = ?,
1233
+ bounded_output = ?, result_reference = ?, action_id = ?, error_code = ?,
1234
+ updated_at = ?
1235
+ WHERE id = ?`)
1236
+ .run(merged.requestedVersion, merged.idempotencyKey, merged.status, merged.step, merged.boundedOutput, merged.resultReference, merged.actionId, merged.errorCode, merged.updatedAt, merged.id);
1237
+ return merged;
1238
+ }
1239
+ markInterruptedInstallJobs() {
1240
+ const result = this.#db
1241
+ .prepare(`UPDATE install_jobs
1242
+ SET status = 'interrupted', step = 'interrupted', updated_at = ?
1243
+ WHERE status IN ('awaiting_secret', 'installing')`)
1244
+ .run(now());
1245
+ return Number(result.changes);
1246
+ }
1247
+ // ── Secure actions (URL-mode secret elicitation) ───────────────────────
1248
+ createSecureAction(input) {
1249
+ const record = secureActionRecordSchema.parse({
1250
+ id: randomUUID(),
1251
+ ...input,
1252
+ createdAt: now(),
1253
+ });
1254
+ this.#db
1255
+ .prepare(`INSERT INTO secure_actions
1256
+ (id, kind, target, principal_id, status, values_json, expires_at, created_at, completed_at)
1257
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1258
+ .run(record.id, record.kind, record.target, record.principalId, record.status, record.valuesJson, record.expiresAt, record.createdAt, record.completedAt);
1259
+ return record;
1260
+ }
1261
+ getSecureAction(id) {
1262
+ const row = this.#db.prepare('SELECT * FROM secure_actions WHERE id = ?').get(id);
1263
+ return row === undefined ? null : this.#parseSecureAction(row);
1264
+ }
1265
+ completeSecureAction(id, valuesJson, completedAt) {
1266
+ const result = this.#db
1267
+ .prepare(`UPDATE secure_actions
1268
+ SET status = 'completed', values_json = ?, completed_at = ?
1269
+ WHERE id = ? AND status = 'pending' AND expires_at > ?`)
1270
+ .run(valuesJson, completedAt, id, completedAt);
1271
+ if (Number(result.changes) === 0) {
1272
+ const current = this.#db
1273
+ .prepare('SELECT id, status FROM secure_actions WHERE id = ?')
1274
+ .get(id);
1275
+ if (current === undefined) {
1276
+ throw new AppError('secure_action_not_found', 'Secure action not found', 404);
1277
+ }
1278
+ throw new AppError('secure_action_used', 'Secure action was already used', 400);
1279
+ }
1280
+ const completed = this.#db.prepare('SELECT * FROM secure_actions WHERE id = ?').get(id);
1281
+ if (completed === undefined) {
1282
+ throw new AppError('secure_action_not_found', 'Secure action not found', 404);
1283
+ }
1284
+ return this.#parseSecureAction(completed);
1285
+ }
1286
+ #callWhere(filter) {
1287
+ const conditions = [];
1288
+ const params = [];
1289
+ if (filter.serverId !== undefined) {
1290
+ conditions.push('server_id = ?');
1291
+ params.push(filter.serverId);
1292
+ }
1293
+ if (filter.tool !== undefined) {
1294
+ conditions.push('upstream_tool_name = ?');
1295
+ params.push(filter.tool);
1296
+ }
1297
+ if (filter.endpointType !== undefined) {
1298
+ conditions.push('endpoint_type = ?');
1299
+ params.push(filter.endpointType);
1300
+ }
1301
+ if (filter.principalId !== undefined) {
1302
+ conditions.push('principal_id = ?');
1303
+ params.push(filter.principalId);
1304
+ }
1305
+ if (filter.status !== undefined) {
1306
+ conditions.push('status = ?');
1307
+ params.push(filter.status);
1308
+ }
1309
+ if (filter.from !== undefined) {
1310
+ conditions.push('started_at >= ?');
1311
+ params.push(filter.from);
1312
+ }
1313
+ if (filter.to !== undefined) {
1314
+ conditions.push('started_at <= ?');
1315
+ params.push(filter.to);
1316
+ }
1317
+ return { where: conditions.length === 0 ? '' : `WHERE ${conditions.join(' AND ')}`, params };
1318
+ }
1319
+ #parseServerProjection(row) {
1320
+ const parsed = serverProjectionRowSchema.parse(row);
1321
+ return serverProjectionSchema.parse({
1322
+ serverId: parsed.server_id,
1323
+ defaultVisibility: parsed.default_visibility,
1324
+ updatedAt: parsed.updated_at,
1325
+ });
1326
+ }
1327
+ #parseToolProjection(row) {
1328
+ const parsed = toolProjectionRowSchema.parse(row);
1329
+ return toolProjectionSchema.parse({
1330
+ serverId: parsed.server_id,
1331
+ upstreamToolName: parsed.upstream_tool_name,
1332
+ visibility: parsed.visibility,
1333
+ updatedAt: parsed.updated_at,
1334
+ });
1335
+ }
1336
+ #parseToolCall(row) {
1337
+ const parsed = toolCallRowSchema.parse(row);
1338
+ return toolCallSchema.parse({
1339
+ id: parsed.id,
1340
+ endpointType: parsed.endpoint_type,
1341
+ principalKind: parsed.principal_kind,
1342
+ principalId: parsed.principal_id,
1343
+ serverId: parsed.server_id,
1344
+ exposedToolName: parsed.exposed_tool_name,
1345
+ upstreamToolName: parsed.upstream_tool_name,
1346
+ status: parsed.status,
1347
+ errorType: parsed.error_type,
1348
+ startedAt: parsed.started_at,
1349
+ completedAt: parsed.completed_at,
1350
+ durationMs: parsed.duration_ms,
1351
+ });
1352
+ }
1353
+ #parseInstallJob(row) {
1354
+ const parsed = installJobRowSchema.parse(row);
1355
+ return installJobRecordSchema.parse({
1356
+ id: parsed.id,
1357
+ entryId: parsed.entry_id,
1358
+ requestedVersion: parsed.requested_version,
1359
+ idempotencyKey: parsed.idempotency_key,
1360
+ status: parsed.status,
1361
+ step: parsed.step,
1362
+ boundedOutput: parsed.bounded_output,
1363
+ resultReference: parsed.result_reference,
1364
+ actionId: parsed.action_id,
1365
+ errorCode: parsed.error_code,
1366
+ createdAt: parsed.created_at,
1367
+ updatedAt: parsed.updated_at,
1368
+ });
1369
+ }
1370
+ #parseInstallation(row) {
1371
+ const parsed = installationRowSchema.parse(row);
1372
+ return marketInstallationSchema.parse({
1373
+ id: parsed.id,
1374
+ source: parsed.source,
1375
+ entryId: parsed.entry_id,
1376
+ entryVersion: parsed.entry_version,
1377
+ recipeRevision: parsed.recipe_revision,
1378
+ targetType: parsed.target_type,
1379
+ targetId: parsed.target_id,
1380
+ credentialId: parsed.credential_id,
1381
+ installedAt: parsed.installed_at,
1382
+ });
1383
+ }
1384
+ #parseSecureAction(row) {
1385
+ const parsed = secureActionRowSchema.parse(row);
1386
+ return secureActionRecordSchema.parse({
1387
+ id: parsed.id,
1388
+ kind: parsed.kind,
1389
+ target: parsed.target,
1390
+ principalId: parsed.principal_id,
1391
+ status: parsed.status,
1392
+ valuesJson: parsed.values_json,
1393
+ expiresAt: parsed.expires_at,
1394
+ createdAt: parsed.created_at,
1395
+ completedAt: parsed.completed_at,
1396
+ });
1397
+ }
1398
+ #parseServer(row) {
1399
+ const parsed = serverRowSchema.parse(row);
1400
+ return serverRecordSchema.parse({
1401
+ id: parsed.id,
1402
+ slug: parsed.slug,
1403
+ name: parsed.name,
1404
+ kind: parsed.kind,
1405
+ transport: parseJson(parsed.transport_json),
1406
+ credentialId: parsed.credential_id,
1407
+ enabled: parsed.enabled === 1,
1408
+ settings: parseJson(parsed.settings_json),
1409
+ createdAt: parsed.created_at,
1410
+ updatedAt: parsed.updated_at,
1411
+ });
1412
+ }
1413
+ #parseCli(row) {
1414
+ const parsed = cliRowSchema.parse(row);
1415
+ return cliRecordSchema.parse({
1416
+ id: parsed.id,
1417
+ slug: parsed.slug,
1418
+ name: parsed.name,
1419
+ command: parsed.command,
1420
+ executionMode: parsed.execution_mode,
1421
+ entrypoint: parsed.entrypoint,
1422
+ authStrategy: (parsed.auth_strategy ?? 'none'),
1423
+ containerVolumes: parseJson(parsed.container_volumes_json ?? '[]'),
1424
+ platform: parsed.platform,
1425
+ allowList: parseJson(parsed.allow_list_json),
1426
+ interactive: parsed.interactive === 1,
1427
+ credentialId: parsed.credential_id,
1428
+ credentialBindings: parseJson(parsed.credential_bindings_json),
1429
+ probe: parsed.probe_json === null ? null : parseJson(parsed.probe_json),
1430
+ enabled: parsed.enabled === 1,
1431
+ timeoutMs: parsed.timeout_ms,
1432
+ maxOutputBytes: parsed.max_output_bytes,
1433
+ createdAt: parsed.created_at,
1434
+ updatedAt: parsed.updated_at,
1435
+ });
1436
+ }
1437
+ #parseCredential(row) {
1438
+ const parsed = credentialRowSchema.parse(row);
1439
+ const status = parsed.type === 'oauth' &&
1440
+ parsed.expires_at !== null &&
1441
+ Date.parse(parsed.expires_at) <= Date.now()
1442
+ ? 'expired'
1443
+ : parsed.status;
1444
+ return credentialRecordSchema.parse({
1445
+ id: parsed.id,
1446
+ name: parsed.name,
1447
+ type: parsed.type,
1448
+ status,
1449
+ expiresAt: parsed.expires_at,
1450
+ createdAt: parsed.created_at,
1451
+ updatedAt: parsed.updated_at,
1452
+ });
1453
+ }
1454
+ #parseApiKey(row) {
1455
+ const parsed = apiKeyRowSchema.parse(row);
1456
+ return apiKeyRecordSchema.parse({
1457
+ id: parsed.id,
1458
+ kind: parsed.kind,
1459
+ name: parsed.name,
1460
+ prefix: parsed.prefix,
1461
+ scope: parsed.scope,
1462
+ createdAt: parsed.created_at,
1463
+ lastUsedAt: parsed.last_used_at,
1464
+ revokedAt: parsed.revoked_at,
1465
+ });
1466
+ }
1467
+ #parseSnapshot(row) {
1468
+ const parsed = snapshotRowSchema.parse(row);
1469
+ return {
1470
+ serverId: parsed.server_id,
1471
+ version: parsed.version,
1472
+ protocolVersion: parsed.protocol_version,
1473
+ protocolEra: z.enum(['modern', 'legacy']).parse(parsed.protocol_era),
1474
+ serverInfo: parsed.server_info_json === null
1475
+ ? null
1476
+ : ImplementationSchema.parse(parseJson(parsed.server_info_json)),
1477
+ capabilities: ServerCapabilitiesSchema.parse(parseJson(parsed.capabilities_json)),
1478
+ instructions: parsed.instructions,
1479
+ tools: z.array(ToolSchema).parse(parseJson(parsed.tools_json)),
1480
+ resources: z.array(ResourceSchema).parse(parseJson(parsed.resources_json)),
1481
+ resourceTemplates: z
1482
+ .array(ResourceTemplateSchema)
1483
+ .parse(parseJson(parsed.resource_templates_json)),
1484
+ prompts: z.array(PromptSchema).parse(parseJson(parsed.prompts_json)),
1485
+ listResults: {
1486
+ tools: ListToolsResultSchema.parse(parseJson(parsed.tools_result_json)),
1487
+ resources: ListResourcesResultSchema.parse(parseJson(parsed.resources_result_json)),
1488
+ resourceTemplates: ListResourceTemplatesResultSchema.parse(parseJson(parsed.resource_templates_result_json)),
1489
+ prompts: ListPromptsResultSchema.parse(parseJson(parsed.prompts_result_json)),
1490
+ },
1491
+ fingerprint: parsed.fingerprint,
1492
+ refreshedAt: parsed.refreshed_at,
1493
+ };
1494
+ }
1495
+ #parseRuntimeState(row) {
1496
+ const parsed = runtimeRowSchema.parse(row);
1497
+ return runtimeStateSchema.parse({
1498
+ serverId: parsed.server_id,
1499
+ status: parsed.status,
1500
+ protocolVersion: parsed.protocol_version,
1501
+ protocolEra: parsed.protocol_era,
1502
+ processId: parsed.process_id,
1503
+ restartCount: parsed.restart_count,
1504
+ lastSuccessAt: parsed.last_success_at,
1505
+ lastError: parsed.last_error,
1506
+ updatedAt: parsed.updated_at,
1507
+ });
1508
+ }
1509
+ #parseEvent(row) {
1510
+ const parsed = eventRowSchema.parse(row);
1511
+ return {
1512
+ id: parsed.id,
1513
+ level: z.enum(['debug', 'info', 'warn', 'error']).parse(parsed.level),
1514
+ type: parsed.type,
1515
+ serverId: parsed.server_id,
1516
+ message: parsed.message,
1517
+ detail: z.record(z.string(), z.unknown()).parse(parseJson(parsed.detail_json)),
1518
+ createdAt: parsed.created_at,
1519
+ };
1520
+ }
1521
+ }
1522
+ //# sourceMappingURL=sqlite-store.js.map