@sundaeswap/sprinkles 0.6.1 → 0.8.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 (197) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  7. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  9. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +744 -0
  11. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  13. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  14. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +711 -0
  15. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  16. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  17. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  18. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  19. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  20. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  21. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  22. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  23. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/index.js +174 -0
  29. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  30. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  31. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  32. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  33. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  34. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  35. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  36. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  37. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  38. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  39. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  40. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  41. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  42. package/dist/cjs/Sprinkle/actions/cli-adapter.js +390 -0
  43. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  44. package/dist/cjs/Sprinkle/actions/index.js +139 -0
  45. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  46. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +557 -0
  47. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  48. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  49. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  50. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  51. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  52. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  53. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  54. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  55. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  56. package/dist/cjs/Sprinkle/index.js +678 -5
  57. package/dist/cjs/Sprinkle/index.js.map +1 -1
  58. package/dist/cjs/Sprinkle/prompts.js +12 -7
  59. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  60. package/dist/cjs/Sprinkle/schemas.js +17 -1
  61. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  62. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  63. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  64. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  65. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  66. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  67. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  68. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  69. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  70. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  71. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  72. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +742 -0
  73. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  74. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  75. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  76. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +710 -0
  77. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  78. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  79. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  80. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  81. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  82. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  83. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  84. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  85. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  86. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  87. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  88. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  89. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  90. package/dist/esm/Sprinkle/actions/builtin/index.js +37 -0
  91. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  92. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  93. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  94. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  95. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  96. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  97. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  98. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  99. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  100. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  101. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  102. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  103. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  104. package/dist/esm/Sprinkle/actions/cli-adapter.js +379 -0
  105. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  106. package/dist/esm/Sprinkle/actions/index.js +12 -0
  107. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  108. package/dist/esm/Sprinkle/actions/mcp-adapter.js +547 -0
  109. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  110. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  111. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  112. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  113. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  114. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  115. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  116. package/dist/esm/Sprinkle/actions/types.js +61 -0
  117. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  118. package/dist/esm/Sprinkle/index.js +517 -7
  119. package/dist/esm/Sprinkle/index.js.map +1 -1
  120. package/dist/esm/Sprinkle/prompts.js +12 -7
  121. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  122. package/dist/esm/Sprinkle/schemas.js +16 -0
  123. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  124. package/dist/esm/Sprinkle/type-guards.js +3 -0
  125. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  126. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  127. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  128. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  129. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  130. package/dist/types/Sprinkle/actions/builtin/index.d.ts +30 -0
  131. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  132. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  133. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  134. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  135. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  136. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  137. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  138. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  139. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  140. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  141. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  142. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  143. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  144. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  145. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  146. package/dist/types/Sprinkle/actions/index.d.ts +13 -0
  147. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  148. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +116 -0
  149. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  150. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  151. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  152. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  153. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  154. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  155. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  156. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  157. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  158. package/dist/types/Sprinkle/index.d.ts +84 -2
  159. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  160. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  161. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  162. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  163. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  164. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  165. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  166. package/package.json +9 -2
  167. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  168. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  169. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  170. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  171. package/src/Sprinkle/__tests__/cli-adapter.test.ts +736 -0
  172. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  173. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +720 -0
  174. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  175. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  176. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  177. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  178. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  179. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  180. package/src/Sprinkle/actions/builtin/index.ts +125 -0
  181. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  182. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  183. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  184. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  185. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  186. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  187. package/src/Sprinkle/actions/cli-adapter.ts +446 -0
  188. package/src/Sprinkle/actions/index.ts +33 -0
  189. package/src/Sprinkle/actions/mcp-adapter.ts +638 -0
  190. package/src/Sprinkle/actions/registry.ts +97 -0
  191. package/src/Sprinkle/actions/runner.ts +200 -0
  192. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  193. package/src/Sprinkle/actions/types.ts +91 -0
  194. package/src/Sprinkle/index.ts +612 -3
  195. package/src/Sprinkle/prompts.ts +118 -72
  196. package/src/Sprinkle/schemas.ts +20 -0
  197. package/src/Sprinkle/type-guards.ts +9 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * MCP adapter for Sprinkles actions.
3
+ *
4
+ * Provides:
5
+ * - TypeBox to JSON Schema conversion (typeboxToJsonSchema)
6
+ * - TypeBox to Zod conversion for MCP SDK compatibility (typeboxToZod)
7
+ * - BigInt string coercion for MCP input (coerceMcpInput)
8
+ * - Lazy MCP SDK import with graceful error (getMcpSdk)
9
+ * - MCP server creation from registered actions (createMcpServer)
10
+ * - MCP orchestrator that starts a stdio server (runMcp)
11
+ */
12
+
13
+ import { createRequire } from "module";
14
+ import { fileURLToPath } from "url";
15
+ import { dirname, resolve } from "path";
16
+ import type { TSchema } from "@sinclair/typebox";
17
+ import { bigIntReplacer } from "../encryption.js";
18
+ import {
19
+ isArray,
20
+ isBigInt,
21
+ isBoolean,
22
+ isInteger,
23
+ isLiteral,
24
+ isNull,
25
+ isNumber,
26
+ isObject,
27
+ isOptional,
28
+ isString,
29
+ isUnion,
30
+ isSensitive,
31
+ } from "../type-guards.js";
32
+ import type { AnyAction } from "./types.js";
33
+ import { executeAction } from "./runner.js";
34
+
35
+ // Re-import Sprinkle as a type only to avoid circular deps
36
+ import type { Sprinkle } from "../index.js";
37
+
38
+ // Load package version for MCP server identification
39
+ // Robustly find package.json - works from both src and dist directories
40
+ function loadPackageVersion(): string {
41
+ const require = createRequire(import.meta.url);
42
+ const __filename = fileURLToPath(import.meta.url);
43
+ const __dirname = dirname(__filename);
44
+
45
+ // Try multiple possible paths (handles src vs dist/esm vs dist/cjs)
46
+ const candidates = [
47
+ resolve(__dirname, "../../../package.json"), // from src/Sprinkle/actions
48
+ resolve(__dirname, "../../../../package.json"), // from dist/*/Sprinkle/actions
49
+ ];
50
+
51
+ for (const candidate of candidates) {
52
+ try {
53
+ const pkg = require(candidate) as { version: string };
54
+ return pkg.version;
55
+ } catch {
56
+ // Try next candidate
57
+ }
58
+ }
59
+
60
+ return "0.0.0"; // Fallback if package.json not found
61
+ }
62
+
63
+ const PACKAGE_VERSION: string = loadPackageVersion();
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // TypeBox to JSON Schema conversion
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Convert a TypeBox schema to a plain JSON Schema object.
71
+ *
72
+ * Strips TypeBox-internal symbols and properties (Kind, OptionalKind, $id)
73
+ * and maps TypeBox-specific types to their JSON Schema equivalents:
74
+ * - BigInt -> { type: "string", pattern: "^-?[0-9]+$" } (JSON has no BigInt)
75
+ * - Sensitive string fields -> add writeOnly: true
76
+ * - Optional -> recurse on inner schema (Optional does not change Kind)
77
+ * - Union -> { anyOf: [...] }
78
+ * - Literal -> { type, const }
79
+ * - Null -> { type: "null" }
80
+ * - Unknown/unsupported -> {} (accepts anything)
81
+ */
82
+ export function typeboxToJsonSchema(schema: TSchema): Record<string, unknown> {
83
+ // Optional in TypeBox doesn't change the Kind, but marks the schema with
84
+ // OptionalKind. We can check for it and recurse transparently since all
85
+ // other guards still work correctly on optional-wrapped schemas.
86
+ //
87
+ // We intentionally do NOT strip optional here -- the containing Object
88
+ // converter decides whether to include a property in `required` based
89
+ // on the isOptional guard applied to the property schema.
90
+
91
+ if (isBigInt(schema)) {
92
+ return {
93
+ type: "string",
94
+ pattern: "^-?[0-9]+$",
95
+ description: "BigInt value as string",
96
+ };
97
+ }
98
+
99
+ if (isString(schema)) {
100
+ const result: Record<string, unknown> = { type: "string" };
101
+ if (schema.description !== undefined) result.description = schema.description;
102
+ if (schema.title !== undefined) result.title = schema.title;
103
+ if (schema.examples !== undefined) result.examples = schema.examples;
104
+ if (schema.pattern !== undefined) result.pattern = schema.pattern;
105
+ if (schema.minLength !== undefined) result.minLength = schema.minLength;
106
+ if (schema.maxLength !== undefined) result.maxLength = schema.maxLength;
107
+ if (schema.default !== undefined) result.default = schema.default;
108
+ // Sensitive fields are write-only (never returned in responses)
109
+ if (isSensitive(schema)) result.writeOnly = true;
110
+ return result;
111
+ }
112
+
113
+ if (isNumber(schema)) {
114
+ const result: Record<string, unknown> = { type: "number" };
115
+ if (schema.minimum !== undefined) result.minimum = schema.minimum;
116
+ if (schema.maximum !== undefined) result.maximum = schema.maximum;
117
+ if (schema.description !== undefined) result.description = schema.description;
118
+ if (schema.default !== undefined) result.default = schema.default;
119
+ return result;
120
+ }
121
+
122
+ if (isInteger(schema)) {
123
+ const result: Record<string, unknown> = { type: "integer" };
124
+ if (schema.minimum !== undefined) result.minimum = schema.minimum;
125
+ if (schema.maximum !== undefined) result.maximum = schema.maximum;
126
+ if (schema.description !== undefined) result.description = schema.description;
127
+ if (schema.default !== undefined) result.default = schema.default;
128
+ return result;
129
+ }
130
+
131
+ if (isBoolean(schema)) {
132
+ const result: Record<string, unknown> = { type: "boolean" };
133
+ if (schema.description !== undefined) result.description = schema.description;
134
+ if (schema.default !== undefined) result.default = schema.default;
135
+ return result;
136
+ }
137
+
138
+ if (isNull(schema)) {
139
+ return { type: "null" };
140
+ }
141
+
142
+ if (isArray(schema)) {
143
+ const result: Record<string, unknown> = { type: "array" };
144
+ const itemSchema = (schema as { items?: TSchema }).items;
145
+ if (itemSchema) {
146
+ result.items = typeboxToJsonSchema(itemSchema);
147
+ }
148
+ if (schema.minItems !== undefined) result.minItems = schema.minItems;
149
+ if (schema.maxItems !== undefined) result.maxItems = schema.maxItems;
150
+ if (schema.description !== undefined) result.description = schema.description;
151
+ return result;
152
+ }
153
+
154
+ if (isObject(schema)) {
155
+ const properties = schema.properties as Record<string, TSchema>;
156
+ const required: string[] = schema.required as string[] ?? [];
157
+ const convertedProperties: Record<string, unknown> = {};
158
+
159
+ for (const [propName, propSchema] of Object.entries(properties)) {
160
+ convertedProperties[propName] = typeboxToJsonSchema(propSchema);
161
+ }
162
+
163
+ const result: Record<string, unknown> = {
164
+ type: "object",
165
+ properties: convertedProperties,
166
+ };
167
+
168
+ if (required.length > 0) {
169
+ result.required = required;
170
+ }
171
+
172
+ if (schema.description !== undefined) result.description = schema.description;
173
+
174
+ return result;
175
+ }
176
+
177
+ if (isUnion(schema)) {
178
+ return {
179
+ anyOf: schema.anyOf.map((member: TSchema) => typeboxToJsonSchema(member)),
180
+ };
181
+ }
182
+
183
+ if (isLiteral(schema)) {
184
+ const value = schema.const;
185
+ let type: string;
186
+ if (typeof value === "string") {
187
+ type = "string";
188
+ } else if (typeof value === "number") {
189
+ type = "number";
190
+ } else if (typeof value === "boolean") {
191
+ type = "boolean";
192
+ } else {
193
+ // Fallback for unexpected literal types
194
+ return {};
195
+ }
196
+ return { type, const: value };
197
+ }
198
+
199
+ // Unknown / unsupported TypeBox type: emit empty schema (accepts anything)
200
+ // This is a safe fallback that allows the MCP tool to still receive input.
201
+ console.warn(
202
+ `[mcp-adapter] Unsupported TypeBox kind: ${(schema as Record<string, unknown>)["[Kind]"] ?? "(unknown)"}. Emitting empty schema.`,
203
+ );
204
+ return {};
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Input coercion: BigInt string -> BigInt
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Coerce MCP JSON input values to the types expected by a TypeBox schema.
213
+ *
214
+ * MCP transmits all values as JSON, which has no BigInt type. When the schema
215
+ * expects a BigInt, the value arrives as a numeric string (e.g. "12345678").
216
+ * This function recursively walks the schema and input, converting BigInt
217
+ * string values to actual BigInt where needed.
218
+ *
219
+ * For all other types the value is passed through unchanged.
220
+ */
221
+ export function coerceMcpInput(input: unknown, schema: TSchema): unknown {
222
+ if (input === null || input === undefined) return input;
223
+
224
+ if (isBigInt(schema)) {
225
+ if (typeof input === "bigint") return input;
226
+ if (typeof input === "string" && /^-?[0-9]+$/.test(input)) {
227
+ try {
228
+ return BigInt(input);
229
+ } catch {
230
+ return input;
231
+ }
232
+ }
233
+ if (typeof input === "number") {
234
+ try {
235
+ return BigInt(Math.trunc(input));
236
+ } catch {
237
+ return input;
238
+ }
239
+ }
240
+ return input;
241
+ }
242
+
243
+ if (isArray(schema)) {
244
+ const itemSchema = (schema as { items?: TSchema }).items;
245
+ if (Array.isArray(input) && itemSchema) {
246
+ return input.map((item) => coerceMcpInput(item, itemSchema));
247
+ }
248
+ return input;
249
+ }
250
+
251
+ if (isObject(schema)) {
252
+ if (typeof input !== "object" || Array.isArray(input)) return input;
253
+ const properties = schema.properties as Record<string, TSchema>;
254
+ const result: Record<string, unknown> = { ...(input as Record<string, unknown>) };
255
+ for (const [propName, propSchema] of Object.entries(properties)) {
256
+ if (propName in result) {
257
+ result[propName] = coerceMcpInput(result[propName], propSchema);
258
+ }
259
+ }
260
+ return result;
261
+ }
262
+
263
+ if (isUnion(schema)) {
264
+ // For unions, we try each member schema in order and return the first
265
+ // successful coercion. BigInt members take priority over string members
266
+ // since they have a narrower, unambiguous pattern.
267
+ for (const member of schema.anyOf) {
268
+ if (isBigInt(member)) {
269
+ if (typeof input === "string" && /^-?[0-9]+$/.test(input)) {
270
+ return coerceMcpInput(input, member);
271
+ }
272
+ }
273
+ }
274
+ // No BigInt match; return as-is for other union members
275
+ return input;
276
+ }
277
+
278
+ // For all other types (String, Number, Integer, Boolean, Literal, Null):
279
+ // pass through unchanged -- MCP JSON already represents them correctly
280
+ return input;
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // TypeBox to Zod conversion
285
+ // ---------------------------------------------------------------------------
286
+
287
+ /**
288
+ * Zod module shape (the parts we use from zod).
289
+ * Typed loosely since zod is a transitive dependency of @modelcontextprotocol/sdk,
290
+ * not a direct dependency of Sprinkles.
291
+ */
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ type ZodType = any;
294
+ interface IZodModule {
295
+ string: () => ZodType;
296
+ number: () => ZodType;
297
+ boolean: () => ZodType;
298
+ literal: (value: unknown) => ZodType;
299
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
+ object: (shape: Record<string, any>) => ZodType;
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ array: (item: any) => ZodType;
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ union: (members: any[]) => ZodType;
305
+ nullType: () => ZodType;
306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
+ any: () => any;
308
+ }
309
+
310
+ let _zod: IZodModule | undefined;
311
+
312
+ /**
313
+ * Lazily import the Zod module.
314
+ * Zod is a transitive dependency of @modelcontextprotocol/sdk, so it's
315
+ * guaranteed to be available when MCP mode is used.
316
+ */
317
+ async function getZod(): Promise<IZodModule> {
318
+ if (_zod) return _zod;
319
+ try {
320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
+ const mod = (await import("zod")) as any;
322
+ _zod = {
323
+ string: () => mod.string(),
324
+ number: () => mod.number(),
325
+ boolean: () => mod.boolean(),
326
+ literal: (v: unknown) => mod.literal(v),
327
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
+ object: (shape: Record<string, any>) => mod.object(shape),
329
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
330
+ array: (item: any) => mod.array(item),
331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
332
+ union: (members: any[]) => mod.union(members),
333
+ nullType: () => mod.nullType(),
334
+ any: () => mod.any(),
335
+ };
336
+ return _zod;
337
+ } catch {
338
+ throw new Error(
339
+ "MCP mode requires zod (via @modelcontextprotocol/sdk). Install @modelcontextprotocol/sdk.",
340
+ );
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Convert a TypeBox schema to a Zod schema for MCP SDK compatibility.
346
+ *
347
+ * The MCP SDK (v1.27+) expects Zod schemas for tool input validation,
348
+ * not plain JSON Schema objects. This function converts TypeBox schemas
349
+ * to their Zod equivalents so the SDK correctly registers tool parameters.
350
+ *
351
+ * BigInt fields are represented as z.string() with a regex pattern since
352
+ * JSON (and thus MCP) has no BigInt type. The coerceMcpInput function
353
+ * handles converting these string values back to BigInt at call time.
354
+ */
355
+ export async function typeboxToZod(schema: TSchema): Promise<ZodType> {
356
+ const z = await getZod();
357
+ return _typeboxToZod(schema, z);
358
+ }
359
+
360
+ function _typeboxToZod(schema: TSchema, z: IZodModule): ZodType {
361
+ if (isBigInt(schema)) {
362
+ // BigInt -> string with regex pattern (JSON has no BigInt)
363
+ return z.string().regex(/^-?[0-9]+$/);
364
+ }
365
+
366
+ if (isString(schema)) {
367
+ let s = z.string();
368
+ if (schema.description) s = s.describe(schema.description);
369
+ return s;
370
+ }
371
+
372
+ if (isNumber(schema) || isInteger(schema)) {
373
+ let n = z.number();
374
+ if (schema.description) n = n.describe(schema.description);
375
+ return n;
376
+ }
377
+
378
+ if (isBoolean(schema)) {
379
+ let b = z.boolean();
380
+ if (schema.description) b = b.describe(schema.description);
381
+ return b;
382
+ }
383
+
384
+ if (isNull(schema)) {
385
+ return z.nullType();
386
+ }
387
+
388
+ if (isLiteral(schema)) {
389
+ return z.literal(schema.const);
390
+ }
391
+
392
+ if (isArray(schema)) {
393
+ const itemSchema = (schema as { items?: TSchema }).items;
394
+ if (itemSchema) {
395
+ return z.array(_typeboxToZod(itemSchema, z));
396
+ }
397
+ return z.array(z.any());
398
+ }
399
+
400
+ if (isObject(schema)) {
401
+ const properties = schema.properties as Record<string, TSchema>;
402
+ const shape: Record<string, ZodType> = {};
403
+
404
+ for (const [propName, propSchema] of Object.entries(properties)) {
405
+ let zodProp = _typeboxToZod(propSchema, z);
406
+ if (isOptional(propSchema)) {
407
+ zodProp = zodProp.optional();
408
+ }
409
+ shape[propName] = zodProp;
410
+ }
411
+
412
+ return z.object(shape);
413
+ }
414
+
415
+ if (isUnion(schema)) {
416
+ const members = schema.anyOf.map((member: TSchema) =>
417
+ _typeboxToZod(member, z),
418
+ );
419
+ if (members.length >= 2) {
420
+ return z.union(members);
421
+ }
422
+ return members[0] ?? z.any();
423
+ }
424
+
425
+ // Fallback: accept anything
426
+ return z.any();
427
+ }
428
+
429
+ /**
430
+ * Convert a TypeBox object schema to a Zod "raw shape" (Record<string, ZodType>).
431
+ * This is the format expected by the MCP SDK's tool() API.
432
+ */
433
+ export async function typeboxToZodShape(
434
+ schema: TSchema,
435
+ ): Promise<Record<string, ZodType>> {
436
+ const z = await getZod();
437
+
438
+ if (!isObject(schema)) {
439
+ // Non-object input schemas get wrapped as { input: zodSchema }
440
+ return { input: _typeboxToZod(schema, z) };
441
+ }
442
+
443
+ const properties = schema.properties as Record<string, TSchema>;
444
+ const shape: Record<string, ZodType> = {};
445
+
446
+ for (const [propName, propSchema] of Object.entries(properties)) {
447
+ let zodProp = _typeboxToZod(propSchema, z);
448
+ if (isOptional(propSchema)) {
449
+ zodProp = zodProp.optional();
450
+ }
451
+ shape[propName] = zodProp;
452
+ }
453
+
454
+ return shape;
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Lazy MCP SDK import
459
+ // ---------------------------------------------------------------------------
460
+
461
+ /**
462
+ * MCP SDK module shape (the parts we use from @modelcontextprotocol/sdk).
463
+ * Typed loosely to avoid a hard dependency at compile time.
464
+ */
465
+ interface IMcpSdkModule {
466
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
467
+ McpServer: new (opts: { name: string; version: string }) => any;
468
+ }
469
+
470
+ interface IStdioTransportModule {
471
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
472
+ StdioServerTransport: new () => any;
473
+ }
474
+
475
+ /**
476
+ * Lazily import the MCP SDK server module.
477
+ *
478
+ * Throws a clear, actionable error if the SDK is not installed, rather than
479
+ * a cryptic "Cannot find module" Node error.
480
+ */
481
+ export async function getMcpSdk(): Promise<IMcpSdkModule> {
482
+ try {
483
+ return (await import(
484
+ "@modelcontextprotocol/sdk/server/mcp.js"
485
+ )) as IMcpSdkModule;
486
+ } catch {
487
+ throw new Error(
488
+ "MCP mode requires @modelcontextprotocol/sdk. Install it with: npm install @modelcontextprotocol/sdk",
489
+ );
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Lazily import the MCP SDK stdio transport module.
495
+ */
496
+ async function getMcpStdioTransport(): Promise<IStdioTransportModule> {
497
+ try {
498
+ return (await import(
499
+ "@modelcontextprotocol/sdk/server/stdio.js"
500
+ )) as IStdioTransportModule;
501
+ } catch {
502
+ throw new Error(
503
+ "MCP mode requires @modelcontextprotocol/sdk. Install it with: npm install @modelcontextprotocol/sdk",
504
+ );
505
+ }
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // MCP server creation
510
+ // ---------------------------------------------------------------------------
511
+
512
+ /**
513
+ * Create an MCP server and register all actions from a Sprinkle instance as
514
+ * MCP tools.
515
+ *
516
+ * Each action becomes a tool with:
517
+ * - name: action.name
518
+ * - description: action.description
519
+ * - input schema: typeboxToJsonSchema(action.inputSchema)
520
+ * - handler: coerces input, executes action, returns JSON result
521
+ *
522
+ * The Sprinkle instance must already be initialized with a profile before
523
+ * this function is called.
524
+ *
525
+ * @param sprinkle - Fully-initialized Sprinkle instance
526
+ * @param serverName - Name to use for the MCP server
527
+ * @returns The configured McpServer instance (not yet connected)
528
+ */
529
+ export async function createMcpServer<S extends TSchema>(
530
+ sprinkle: Sprinkle<S>,
531
+ serverName: string,
532
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
533
+ ): Promise<any> {
534
+ const { McpServer } = await getMcpSdk();
535
+
536
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
537
+ const server = new McpServer({ name: serverName, version: PACKAGE_VERSION });
538
+
539
+ const actions = sprinkle.listActions() as AnyAction<S>[];
540
+
541
+ for (const action of actions) {
542
+ const zodShape = await typeboxToZodShape(action.inputSchema);
543
+
544
+ // Register the action as an MCP tool.
545
+ // The high-level McpServer.tool() API accepts a Zod raw shape
546
+ // (Record<string, ZodType>) for input validation.
547
+ server.tool(
548
+ action.name,
549
+ action.description,
550
+ zodShape,
551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
552
+ async (input: unknown): Promise<any> => {
553
+ const coercedInput = coerceMcpInput(input, action.inputSchema);
554
+
555
+ const context = {
556
+ sprinkle,
557
+ settings: sprinkle.settings,
558
+ };
559
+
560
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
561
+ const result = await executeAction(action as any, coercedInput, context as any);
562
+
563
+ if (result.success) {
564
+ return {
565
+ content: [
566
+ {
567
+ type: "text",
568
+ text: JSON.stringify(result.data, bigIntReplacer),
569
+ },
570
+ ],
571
+ };
572
+ } else {
573
+ return {
574
+ content: [
575
+ {
576
+ type: "text",
577
+ text: JSON.stringify(
578
+ {
579
+ error: {
580
+ code: result.error.code,
581
+ message: result.error.message,
582
+ details: result.error.details,
583
+ },
584
+ },
585
+ bigIntReplacer,
586
+ ),
587
+ },
588
+ ],
589
+ isError: true,
590
+ };
591
+ }
592
+ },
593
+ );
594
+ }
595
+
596
+ return server;
597
+ }
598
+
599
+ // ---------------------------------------------------------------------------
600
+ // MCP orchestrator
601
+ // ---------------------------------------------------------------------------
602
+
603
+ /**
604
+ * Start an MCP server on stdio transport.
605
+ *
606
+ * This function:
607
+ * 1. Creates the MCP server and registers all actions as tools
608
+ * 2. Creates a StdioServerTransport
609
+ * 3. Connects the server to the transport
610
+ *
611
+ * IMPORTANT: All logging in this function goes to stderr. stdout is reserved
612
+ * for the MCP protocol messages.
613
+ *
614
+ * The Sprinkle instance must already be initialized with a profile. Profile
615
+ * initialization is the responsibility of the caller (Sprinkle.Run()).
616
+ *
617
+ * @param sprinkle - Fully-initialized Sprinkle instance
618
+ * @param serverName - Name to use for the MCP server
619
+ */
620
+ export async function runMcp<S extends TSchema>(
621
+ sprinkle: Sprinkle<S>,
622
+ serverName: string,
623
+ ): Promise<void> {
624
+ const { StdioServerTransport } = await getMcpStdioTransport();
625
+
626
+ const server = await createMcpServer(sprinkle, serverName);
627
+ const transport = new StdioServerTransport();
628
+
629
+ try {
630
+ await server.connect(transport);
631
+ } catch (err) {
632
+ // Log errors to stderr only -- stdout is the MCP transport channel
633
+ process.stderr.write(
634
+ `[sprinkle-mcp] Failed to start MCP server: ${err instanceof Error ? err.message : String(err)}\n`,
635
+ );
636
+ throw err;
637
+ }
638
+ }