@lovrozagar/honey 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
@@ -0,0 +1,364 @@
1
+ /* MCP server code generator. Spec-driven (not IR-driven) — OpenAPI parameters[]
2
+ * + requestBody.content['application/json'].schema are already JSON Schema,
3
+ * no translation layer needed. Mirrors codegen-python.ts structure. */
4
+
5
+ import { readFileSync } from "node:fs"
6
+ import type { OpenApiSpecInput } from "./codegen.ts"
7
+ import { toIR } from "./codegen-ir.ts"
8
+
9
+ type MCPOptions = {
10
+ projectName: string
11
+ sdkPackageName: string
12
+ sdkClassName: string
13
+ version?: string
14
+ }
15
+
16
+ type MCPResult = {
17
+ files: Record<string, string>
18
+ }
19
+
20
+ type MCPOp = {
21
+ operationId: string
22
+ method: string
23
+ path: string
24
+ summary?: string
25
+ description?: string
26
+ parameters: Array<Record<string, unknown>>
27
+ bodySchema: Record<string, unknown> | undefined
28
+ bodyRequired: boolean
29
+ toolName: string
30
+ pathSegments: string[]
31
+ }
32
+
33
+ type SpecRecord = Record<string, unknown>
34
+
35
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const
36
+
37
+ const TEMPLATE_FILE_NAMES = ["server.ts", "types.ts"] as const
38
+ const TEMPLATE_CACHE = new Map<string, string>()
39
+
40
+ function loadMCPRuntimeTemplates(): Map<string, string> {
41
+ if (TEMPLATE_CACHE.size > 0) return TEMPLATE_CACHE
42
+ for (const name of TEMPLATE_FILE_NAMES) {
43
+ const url = new URL(`./client-mcp/${name}`, import.meta.url)
44
+ TEMPLATE_CACHE.set(name, readFileSync(url, "utf8"))
45
+ }
46
+ return TEMPLATE_CACHE
47
+ }
48
+
49
+ function envVarNames(projectName: string): { apiKey: string; baseUrl: string } {
50
+ const upper = projectName.toUpperCase()
51
+ return { apiKey: `${upper}_API_KEY`, baseUrl: `${upper}_BASE_URL` }
52
+ }
53
+
54
+ function hasSSEResponse(op: SpecRecord): boolean {
55
+ const responses = op.responses as Record<string, SpecRecord> | undefined
56
+ if (!responses) return false
57
+ for (const resp of Object.values(responses)) {
58
+ const content = resp.content as SpecRecord | undefined
59
+ if (content && "text/event-stream" in content) return true
60
+ }
61
+ return false
62
+ }
63
+
64
+ function toSnakeCase(id: string): string {
65
+ /* camelCase + dots -> snake_case. "createUser" -> "create_user",
66
+ * "docs.extract" -> "docs_extract", "docs.listAll" -> "docs_list_all". */
67
+ return id
68
+ .replace(/\./g, "_")
69
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
70
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
71
+ .toLowerCase()
72
+ }
73
+
74
+ function resolveSchema(
75
+ spec: SpecRecord,
76
+ schema: SpecRecord | undefined,
77
+ ): SpecRecord | undefined {
78
+ if (!schema) return undefined
79
+ if (!schema.$ref) return schema
80
+ const ref = schema.$ref as string
81
+ const parts = ref.replace(/^#\//, "").split("/")
82
+ let cur: unknown = spec
83
+ for (const part of parts) {
84
+ if (cur === null || typeof cur !== "object") return undefined
85
+ cur = (cur as SpecRecord)[part]
86
+ }
87
+ if (cur === undefined) return undefined
88
+ /* recurse — resolved schema may itself be a ref */
89
+ return resolveSchema(spec, cur as SpecRecord)
90
+ }
91
+
92
+ function deepResolveSchema(
93
+ spec: SpecRecord,
94
+ schema: SpecRecord | undefined,
95
+ seen: Set<string> = new Set(),
96
+ ): SpecRecord | undefined {
97
+ if (!schema) return undefined
98
+ if (schema.$ref) {
99
+ const ref = schema.$ref as string
100
+ if (seen.has(ref)) return { type: "object" }
101
+ const next = new Set(seen)
102
+ next.add(ref)
103
+ return deepResolveSchema(spec, resolveSchema(spec, schema), next)
104
+ }
105
+ const out: SpecRecord = {}
106
+ for (const [k, v] of Object.entries(schema)) {
107
+ if (v === null || v === undefined) {
108
+ out[k] = v
109
+ } else if (Array.isArray(v)) {
110
+ out[k] = v.map((item) =>
111
+ item !== null && typeof item === "object"
112
+ ? deepResolveSchema(spec, item as SpecRecord, seen)
113
+ : item,
114
+ )
115
+ } else if (typeof v === "object") {
116
+ out[k] = deepResolveSchema(spec, v as SpecRecord, seen)
117
+ } else {
118
+ out[k] = v
119
+ }
120
+ }
121
+ return out
122
+ }
123
+
124
+ function extractJsonBodySchema(op: SpecRecord): SpecRecord | undefined {
125
+ const requestBody = op.requestBody as SpecRecord | undefined
126
+ const content = requestBody?.content as Record<string, SpecRecord> | undefined
127
+ return content?.["application/json"]?.schema as SpecRecord | undefined
128
+ }
129
+
130
+ function collectMCPOps(spec: OpenApiSpecInput): MCPOp[] {
131
+ const paths = spec.paths as Record<string, SpecRecord> | undefined
132
+ if (!paths) return []
133
+ const ops: MCPOp[] = []
134
+
135
+ for (const [path, pathItem] of Object.entries(paths)) {
136
+ for (const method of HTTP_METHODS) {
137
+ const op = pathItem[method] as SpecRecord | undefined
138
+ if (!op) continue
139
+ if (op["x-mcp"] !== true) continue
140
+ /* skip websocket / realtime / SSE operations — MCP is request/response only */
141
+ if (op["x-websocket"] === true) continue
142
+ if (op["x-realtime"] === true) continue
143
+ if (hasSSEResponse(op)) continue
144
+
145
+ const operationId = op.operationId as string | undefined
146
+ if (!operationId) continue
147
+
148
+ ops.push({
149
+ bodyRequired: (op.requestBody as SpecRecord | undefined)?.required === true,
150
+ bodySchema: extractJsonBodySchema(op),
151
+ description: op.description as string | undefined,
152
+ method: method.toUpperCase(),
153
+ operationId,
154
+ parameters: (op.parameters as Array<SpecRecord> | undefined) ?? [],
155
+ path,
156
+ pathSegments: operationId.split("."),
157
+ summary: op.summary as string | undefined,
158
+ toolName: "",
159
+ })
160
+ }
161
+ }
162
+
163
+ return ops
164
+ }
165
+
166
+ type ParamGroup = { props: SpecRecord; required: string[] }
167
+
168
+ function emptyParamGroup(): ParamGroup {
169
+ return { props: {}, required: [] }
170
+ }
171
+
172
+ function assignGroup(
173
+ properties: SpecRecord,
174
+ required: string[],
175
+ key: string,
176
+ group: ParamGroup,
177
+ ): void {
178
+ if (Object.keys(group.props).length === 0) return
179
+ const out: SpecRecord = { properties: group.props, type: "object" }
180
+ if (group.required.length > 0) {
181
+ out.required = group.required
182
+ required.push(key)
183
+ }
184
+ properties[key] = out
185
+ }
186
+
187
+ function buildToolInputSchema(
188
+ op: MCPOp,
189
+ spec: OpenApiSpecInput,
190
+ ): SpecRecord {
191
+ const properties: SpecRecord = {}
192
+ const required: string[] = []
193
+ const specRec = spec as unknown as SpecRecord
194
+
195
+ const path = emptyParamGroup()
196
+ const query = emptyParamGroup()
197
+ const header = emptyParamGroup()
198
+ const groupByLoc: Record<string, ParamGroup> = { header, path, query }
199
+
200
+ for (const p of op.parameters) {
201
+ const name = p.name as string
202
+ const inLoc = p.in as string
203
+ const group = groupByLoc[inLoc]
204
+ if (!group) continue
205
+ group.props[name] = deepResolveSchema(specRec, (p.schema ?? {}) as SpecRecord) ?? {}
206
+ if (p.required === true) group.required.push(name)
207
+ }
208
+
209
+ assignGroup(properties, required, "params", path)
210
+ assignGroup(properties, required, "search", query)
211
+ assignGroup(properties, required, "headers", header)
212
+
213
+ if (op.bodySchema) {
214
+ properties.json = deepResolveSchema(specRec, op.bodySchema) ?? { type: "object" }
215
+ if (op.bodyRequired) required.push("json")
216
+ }
217
+
218
+ const schema: SpecRecord = { properties, type: "object" }
219
+ if (required.length > 0) schema.required = required
220
+ return schema
221
+ }
222
+
223
+ function buildToolsGen(
224
+ ops: MCPOp[],
225
+ spec: OpenApiSpecInput,
226
+ projectName: string,
227
+ ): string {
228
+ const l: string[] = []
229
+ l.push(`/* Generated by honey codegen-mcp.ts — do not edit by hand. */`)
230
+ l.push(``)
231
+ l.push(`import type { Tool } from "./types"`)
232
+ l.push(``)
233
+ l.push(`export function buildTools(sdk: Record<string, unknown>): Tool[] {`)
234
+ l.push(`\treturn [`)
235
+
236
+ for (const op of ops) {
237
+ const desc = op.description ?? op.summary ?? ""
238
+ const inputSchema = buildToolInputSchema(op, spec)
239
+ l.push(`\t\t{`)
240
+ l.push(`\t\t\tname: ${JSON.stringify(op.toolName)},`)
241
+ if (desc.length > 0) {
242
+ l.push(`\t\t\tdescription: ${JSON.stringify(desc)},`)
243
+ }
244
+ l.push(`\t\t\tinputSchema: ${JSON.stringify(inputSchema)},`)
245
+ /* handler dispatches by walking all path segments; pass args verbatim.
246
+ * arguments shape already matches TS SDK method signature: { params?, search?, headers?, json? }. */
247
+ const callExpr = op.pathSegments.reduce(
248
+ (acc, seg, i) =>
249
+ i === 0
250
+ ? `(sdk as Record<string, unknown>)[${JSON.stringify(seg)}]`
251
+ : `(${acc} as Record<string, unknown>)[${JSON.stringify(seg)}]`,
252
+ "",
253
+ )
254
+ l.push(`\t\t\thandler: async (args) => {`)
255
+ l.push(`\t\t\t\tconst method = ${callExpr}`)
256
+ l.push(`\t\t\t\tif (typeof method !== "function") {`)
257
+ l.push(`\t\t\t\t\treturn { content: [{ type: "text", text: JSON.stringify({ message: "SDK missing method ${op.operationId}" }) }], isError: true }`)
258
+ l.push(`\t\t\t\t}`)
259
+ l.push(`\t\t\t\tconst result = await method.call(sdk, args)`)
260
+ l.push(`\t\t\t\treturn { content: [{ type: "text", text: JSON.stringify(result) }] }`)
261
+ l.push(`\t\t\t},`)
262
+ l.push(`\t\t},`)
263
+ }
264
+
265
+ l.push(`\t]`)
266
+ l.push(`}`)
267
+ l.push(``)
268
+ /* project name embedded for tool-naming contract visibility — consumed by tests */
269
+ l.push(`/* project: ${projectName} */`)
270
+ l.push(``)
271
+ return l.join("\n")
272
+ }
273
+
274
+ function buildServerEntry(
275
+ projectName: string,
276
+ sdkPackageName: string,
277
+ sdkClassName: string,
278
+ serverName: string,
279
+ version: string,
280
+ ): string {
281
+ const { apiKey: envVar, baseUrl: baseUrlEnvVar } = envVarNames(projectName)
282
+ const defaultBaseUrl = `https://api.${projectName}.com`
283
+ const l: string[] = []
284
+ l.push(`/* Generated by honey codegen-mcp.ts — do not edit by hand. */`)
285
+ l.push(``)
286
+ l.push(`import { ${sdkClassName} } from ${JSON.stringify(sdkPackageName)}`)
287
+ l.push(`import { createMCPServer } from "./_runtime"`)
288
+ l.push(`import { buildTools } from "./tools.gen"`)
289
+ l.push(``)
290
+ l.push(`const apiKey = process.env[${JSON.stringify(envVar)}] ?? ""`)
291
+ l.push(`const baseURL = process.env[${JSON.stringify(baseUrlEnvVar)}] ?? ${JSON.stringify(defaultBaseUrl)}`)
292
+ l.push(``)
293
+ l.push(`const sdk = new ${sdkClassName}({`)
294
+ l.push(`\tbaseURL,`)
295
+ l.push(`\theaders: apiKey.length > 0 ? { Authorization: \`Bearer \${apiKey}\` } : {},`)
296
+ l.push(`\tthrowOnError: true,`)
297
+ l.push(`})`)
298
+ l.push(``)
299
+ l.push(`const tools = buildTools(sdk as unknown as Record<string, unknown>)`)
300
+ l.push(``)
301
+ l.push(`await createMCPServer({`)
302
+ l.push(`\tsdk,`)
303
+ l.push(`\tserverInfo: { name: ${JSON.stringify(serverName)}, version: ${JSON.stringify(version)} },`)
304
+ l.push(`\ttools,`)
305
+ l.push(`})`)
306
+ l.push(``)
307
+ return l.join("\n")
308
+ }
309
+
310
+ function buildTsconfig(): string {
311
+ return JSON.stringify(
312
+ {
313
+ compilerOptions: {
314
+ allowImportingTsExtensions: false,
315
+ declaration: true,
316
+ esModuleInterop: true,
317
+ module: "ESNext",
318
+ moduleResolution: "bundler",
319
+ noEmit: false,
320
+ outDir: "./dist",
321
+ rootDir: "./src",
322
+ skipLibCheck: true,
323
+ strict: true,
324
+ target: "ES2022",
325
+ types: ["node"],
326
+ },
327
+ include: ["src/**/*"],
328
+ },
329
+ null,
330
+ 2,
331
+ )
332
+ }
333
+
334
+ export function generateMCPServer(
335
+ spec: OpenApiSpecInput,
336
+ options: MCPOptions,
337
+ ): MCPResult {
338
+ toIR(spec)
339
+ const { projectName, sdkClassName, sdkPackageName } = options
340
+ const version = options.version ?? "0.1.0"
341
+
342
+ const ops = collectMCPOps(spec)
343
+ if (ops.length === 0) {
344
+ throw new Error("No operations marked x-mcp: true; nothing to emit")
345
+ }
346
+ for (const op of ops) {
347
+ op.toolName = `${projectName}_${toSnakeCase(op.operationId)}`
348
+ }
349
+
350
+ const info = (spec as { info?: { title?: string } }).info ?? {}
351
+ const serverName = info.title ?? `@${projectName}/mcp-server`
352
+
353
+ const templates = loadMCPRuntimeTemplates()
354
+
355
+ return {
356
+ files: {
357
+ "src/_runtime.ts": templates.get("server.ts") ?? "",
358
+ "src/server.ts": buildServerEntry(projectName, sdkPackageName, sdkClassName, serverName, version),
359
+ "src/tools.gen.ts": buildToolsGen(ops, spec, projectName),
360
+ "src/types.ts": templates.get("types.ts") ?? "",
361
+ "tsconfig.json": buildTsconfig(),
362
+ },
363
+ }
364
+ }