@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,452 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+
3
+ package sdk
4
+
5
+ import (
6
+ "context"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "net/url"
13
+ "strings"
14
+ "sync"
15
+
16
+ "nhooyr.io/websocket"
17
+ )
18
+
19
+ // TransportKind tags which transport a connection was opened with.
20
+ // Mirrors Rust TransportKind / Python TransportKind.
21
+ type TransportKind string
22
+
23
+ const (
24
+ TransportWs TransportKind = "ws"
25
+ TransportSse TransportKind = "sse"
26
+ TransportLongpoll TransportKind = "longpoll"
27
+ )
28
+
29
+ // TransportOpts is handed to each Transport.Connect call. Mirrors the TS
30
+ // TransportOpts plus per-language extras (HTTPClient for SSE/longpoll dial).
31
+ type TransportOpts struct {
32
+ ReconnectToken string
33
+ LastEventID string
34
+ Headers http.Header
35
+ MaxReconnectAttempts int
36
+ ReconnectDelayMs int
37
+ Protocols []string
38
+ HTTPClient *http.Client
39
+ }
40
+
41
+ // TransportConn is the live per-transport connection object. Each built-in
42
+ // adapter returns its own impl (wsConn, sseConn, longpollConn). Recv returns
43
+ // decoded JSON (map[string]any, []any, string, number, bool) or raw string
44
+ // fallback when the payload is not JSON.
45
+ type TransportConn interface {
46
+ Recv(ctx context.Context) (any, error)
47
+ Send(ctx context.Context, data any) error
48
+ Close() error
49
+ Kind() TransportKind
50
+ }
51
+
52
+ // Transport is the pluggable adapter interface. Consumers swap in their own
53
+ // WS/SSE/longpoll impls by implementing this. Mirrors Rust trait Transport
54
+ // and Python TransportAdapter Protocol.
55
+ type Transport interface {
56
+ Name() string
57
+ Kind() TransportKind
58
+ Connect(ctx context.Context, url string, opts *TransportOpts) (TransportConn, error)
59
+ }
60
+
61
+ /* errTransportNotImplemented remains as a transitional sentinel from the 8.b.3
62
+ * stub phase. Real Connect bodies below no longer return it; kept to avoid
63
+ * breaking any consumer that wrote defensive errors.Is checks. Removal deferred
64
+ * to 8.d cleanup. */
65
+ var errTransportNotImplemented = errors.New("honey: transport adapter Connect not implemented")
66
+
67
+ /* ───────────────────────── WebSocket adapter ───────────────────────── */
68
+
69
+ // WsTransport is the default WebSocket adapter. Wraps nhooyr.io/websocket.
70
+ type WsTransport struct {
71
+ Subprotocols []string
72
+ }
73
+
74
+ // Name returns "ws".
75
+ func (t *WsTransport) Name() string { return "ws" }
76
+
77
+ // Kind returns TransportWs.
78
+ func (t *WsTransport) Kind() TransportKind { return TransportWs }
79
+
80
+ // Connect dials the WebSocket endpoint. Auth + resume tokens ride as query
81
+ // params on the URL; protocols + headers come from opts. Returned conn is a
82
+ // *wsConn satisfying TransportConn.
83
+ func (t *WsTransport) Connect(ctx context.Context, rawURL string, opts *TransportOpts) (TransportConn, error) {
84
+ wsURL := toWsURL(rawURL)
85
+ if opts != nil {
86
+ q := url.Values{}
87
+ if opts.ReconnectToken != "" {
88
+ q.Set("reconnect_token", opts.ReconnectToken)
89
+ }
90
+ if opts.LastEventID != "" {
91
+ q.Set("last_event_id", opts.LastEventID)
92
+ }
93
+ if len(q) > 0 {
94
+ sep := "?"
95
+ if strings.Contains(wsURL, "?") {
96
+ sep = "&"
97
+ }
98
+ wsURL = wsURL + sep + q.Encode()
99
+ }
100
+ }
101
+ dialOpts := &websocket.DialOptions{}
102
+ if opts != nil {
103
+ if len(opts.Protocols) > 0 {
104
+ dialOpts.Subprotocols = opts.Protocols
105
+ }
106
+ if len(opts.Headers) > 0 {
107
+ dialOpts.HTTPHeader = opts.Headers.Clone()
108
+ }
109
+ }
110
+ if dialOpts.Subprotocols == nil && len(t.Subprotocols) > 0 {
111
+ dialOpts.Subprotocols = t.Subprotocols
112
+ }
113
+ conn, _, err := websocket.Dial(ctx, wsURL, dialOpts)
114
+ if err != nil {
115
+ return nil, err
116
+ }
117
+ return &wsConn{conn: conn}, nil
118
+ }
119
+
120
+ type wsConn struct {
121
+ conn *websocket.Conn
122
+ }
123
+
124
+ func (c *wsConn) Kind() TransportKind { return TransportWs }
125
+
126
+ func (c *wsConn) Recv(ctx context.Context) (any, error) {
127
+ _, data, err := c.conn.Read(ctx)
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+ var v any
132
+ if jsonErr := json.Unmarshal(data, &v); jsonErr != nil {
133
+ /* non-JSON payload — fall through to raw string (matches Python decode). */
134
+ return string(data), nil
135
+ }
136
+ return v, nil
137
+ }
138
+
139
+ func (c *wsConn) Send(ctx context.Context, data any) error {
140
+ switch v := data.(type) {
141
+ case []byte:
142
+ return c.conn.Write(ctx, websocket.MessageBinary, v)
143
+ case string:
144
+ return c.conn.Write(ctx, websocket.MessageText, []byte(v))
145
+ default:
146
+ encoded, err := json.Marshal(v)
147
+ if err != nil {
148
+ return err
149
+ }
150
+ return c.conn.Write(ctx, websocket.MessageText, encoded)
151
+ }
152
+ }
153
+
154
+ func (c *wsConn) Close() error {
155
+ return c.conn.Close(websocket.StatusNormalClosure, "client close")
156
+ }
157
+
158
+ func toWsURL(u string) string {
159
+ if strings.HasPrefix(u, "https://") {
160
+ return "wss://" + u[len("https://"):]
161
+ }
162
+ if strings.HasPrefix(u, "http://") {
163
+ return "ws://" + u[len("http://"):]
164
+ }
165
+ return u
166
+ }
167
+
168
+ /* ────────────────────────────── SSE adapter ────────────────────────────── */
169
+
170
+ // SseTransport is the default SSE adapter. Uses net/http streaming GET via
171
+ // parseSSEStream. A background goroutine bridges the iterator to a Recv channel
172
+ // so ResumableConnection.Recv has uniform blocking semantics across transports.
173
+ type SseTransport struct {
174
+ HTTPClient *http.Client
175
+ }
176
+
177
+ // Name returns "sse".
178
+ func (t *SseTransport) Name() string { return "sse" }
179
+
180
+ // Kind returns TransportSse.
181
+ func (t *SseTransport) Kind() TransportKind { return TransportSse }
182
+
183
+ // Connect opens the SSE byte stream. Reconnect token rides as a query param;
184
+ // Last-Event-ID rides as a request header (HTML spec).
185
+ func (t *SseTransport) Connect(ctx context.Context, rawURL string, opts *TransportOpts) (TransportConn, error) {
186
+ client := t.HTTPClient
187
+ if opts != nil && opts.HTTPClient != nil {
188
+ client = opts.HTTPClient
189
+ }
190
+ if client == nil {
191
+ client = http.DefaultClient
192
+ }
193
+
194
+ u := rawURL
195
+ if opts != nil && opts.ReconnectToken != "" {
196
+ sep := "?"
197
+ if strings.Contains(u, "?") {
198
+ sep = "&"
199
+ }
200
+ u = u + sep + "reconnect_token=" + url.QueryEscape(opts.ReconnectToken)
201
+ }
202
+ req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
203
+ if err != nil {
204
+ return nil, err
205
+ }
206
+ req.Header.Set("Accept", "text/event-stream")
207
+ if opts != nil {
208
+ for k, vv := range opts.Headers {
209
+ for _, v := range vv {
210
+ req.Header.Add(k, v)
211
+ }
212
+ }
213
+ if opts.LastEventID != "" {
214
+ req.Header.Set("Last-Event-ID", opts.LastEventID)
215
+ }
216
+ }
217
+ resp, err := client.Do(req)
218
+ if err != nil {
219
+ return nil, err
220
+ }
221
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
222
+ _ = resp.Body.Close()
223
+ return nil, fmt.Errorf("honey: sse transport: status %d", resp.StatusCode)
224
+ }
225
+ sc := &sseConn{
226
+ resp: resp,
227
+ next: make(chan sseResult, 1),
228
+ done: make(chan struct{}),
229
+ }
230
+ go sc.run(ctx)
231
+ return sc, nil
232
+ }
233
+
234
+ type sseResult struct {
235
+ value any
236
+ err error
237
+ }
238
+
239
+ type sseConn struct {
240
+ resp *http.Response
241
+ next chan sseResult
242
+ done chan struct{}
243
+ mu sync.Mutex
244
+ closed bool
245
+ }
246
+
247
+ func (c *sseConn) Kind() TransportKind { return TransportSse }
248
+
249
+ func (c *sseConn) run(ctx context.Context) {
250
+ defer c.finish()
251
+ for ev, err := range parseSSEStream(ctx, c.resp) {
252
+ select {
253
+ case <-c.done:
254
+ return
255
+ default:
256
+ }
257
+ if err != nil {
258
+ select {
259
+ case c.next <- sseResult{err: err}:
260
+ case <-c.done:
261
+ }
262
+ return
263
+ }
264
+ var v any
265
+ if jsonErr := json.Unmarshal([]byte(ev.Data), &v); jsonErr != nil {
266
+ v = ev.Data
267
+ }
268
+ select {
269
+ case c.next <- sseResult{value: v}:
270
+ case <-c.done:
271
+ return
272
+ }
273
+ }
274
+ }
275
+
276
+ func (c *sseConn) finish() {
277
+ c.mu.Lock()
278
+ defer c.mu.Unlock()
279
+ if c.closed {
280
+ return
281
+ }
282
+ c.closed = true
283
+ close(c.next)
284
+ }
285
+
286
+ func (c *sseConn) Recv(ctx context.Context) (any, error) {
287
+ select {
288
+ case r, ok := <-c.next:
289
+ if !ok {
290
+ return nil, io.EOF
291
+ }
292
+ return r.value, r.err
293
+ case <-ctx.Done():
294
+ return nil, ctx.Err()
295
+ }
296
+ }
297
+
298
+ func (c *sseConn) Send(ctx context.Context, data any) error {
299
+ return errors.New("honey: sse transport is receive-only")
300
+ }
301
+
302
+ func (c *sseConn) Close() error {
303
+ c.mu.Lock()
304
+ alreadyClosed := c.closed
305
+ c.mu.Unlock()
306
+ /* signal producer, then close body — parseSSEStream closes body on its own
307
+ * iterator exit, but bodyClose here is idempotent for the already-closed
308
+ * case via Body.Close()'s stdlib contract. */
309
+ select {
310
+ case <-c.done:
311
+ default:
312
+ close(c.done)
313
+ }
314
+ if !alreadyClosed {
315
+ return c.resp.Body.Close()
316
+ }
317
+ return nil
318
+ }
319
+
320
+ /* ──────────────────────────── Longpoll adapter ─────────────────────────── */
321
+
322
+ // LongpollTransport is the default long-poll adapter. Each Recv fires a GET
323
+ // against the configured URL with cursor query params and blocks until the
324
+ // server replies. Payloads that look like JSON objects have their
325
+ // reconnect_token / id / last_event_id fields harvested to advance the cursor.
326
+ type LongpollTransport struct {
327
+ HTTPClient *http.Client
328
+ }
329
+
330
+ // Name returns "longpoll".
331
+ func (t *LongpollTransport) Name() string { return "longpoll" }
332
+
333
+ // Kind returns TransportLongpoll.
334
+ func (t *LongpollTransport) Kind() TransportKind { return TransportLongpoll }
335
+
336
+ // Connect constructs the longpoll conn; no wire traffic until first Recv.
337
+ func (t *LongpollTransport) Connect(ctx context.Context, rawURL string, opts *TransportOpts) (TransportConn, error) {
338
+ client := t.HTTPClient
339
+ if opts != nil && opts.HTTPClient != nil {
340
+ client = opts.HTTPClient
341
+ }
342
+ if client == nil {
343
+ client = http.DefaultClient
344
+ }
345
+ lp := &longpollConn{
346
+ client: client,
347
+ url: rawURL,
348
+ headers: http.Header{},
349
+ }
350
+ if opts != nil {
351
+ lp.reconnectToken = opts.ReconnectToken
352
+ lp.lastEventID = opts.LastEventID
353
+ if opts.Headers != nil {
354
+ lp.headers = opts.Headers.Clone()
355
+ }
356
+ }
357
+ return lp, nil
358
+ }
359
+
360
+ type longpollConn struct {
361
+ client *http.Client
362
+ url string
363
+ headers http.Header
364
+ reconnectToken string
365
+ lastEventID string
366
+ mu sync.Mutex
367
+ closed bool
368
+ }
369
+
370
+ func (c *longpollConn) Kind() TransportKind { return TransportLongpoll }
371
+
372
+ func (c *longpollConn) Recv(ctx context.Context) (any, error) {
373
+ c.mu.Lock()
374
+ if c.closed {
375
+ c.mu.Unlock()
376
+ return nil, io.EOF
377
+ }
378
+ token := c.reconnectToken
379
+ lastID := c.lastEventID
380
+ headers := c.headers
381
+ c.mu.Unlock()
382
+
383
+ q := url.Values{}
384
+ if token != "" {
385
+ q.Set("reconnect_token", token)
386
+ }
387
+ if lastID != "" {
388
+ q.Set("last_event_id", lastID)
389
+ }
390
+ u := c.url
391
+ if len(q) > 0 {
392
+ sep := "?"
393
+ if strings.Contains(u, "?") {
394
+ sep = "&"
395
+ }
396
+ u = u + sep + q.Encode()
397
+ }
398
+ req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
399
+ if err != nil {
400
+ return nil, err
401
+ }
402
+ for k, vv := range headers {
403
+ for _, v := range vv {
404
+ req.Header.Add(k, v)
405
+ }
406
+ }
407
+ resp, err := c.client.Do(req)
408
+ if err != nil {
409
+ return nil, err
410
+ }
411
+ defer resp.Body.Close()
412
+ body, err := io.ReadAll(resp.Body)
413
+ if err != nil {
414
+ return nil, err
415
+ }
416
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
417
+ return nil, fmt.Errorf("honey: longpoll transport: status %d", resp.StatusCode)
418
+ }
419
+ var v any
420
+ if jsonErr := json.Unmarshal(body, &v); jsonErr != nil {
421
+ v = string(body)
422
+ }
423
+ if m, ok := v.(map[string]any); ok {
424
+ c.mu.Lock()
425
+ if tok, ok := m["reconnect_token"].(string); ok {
426
+ c.reconnectToken = tok
427
+ }
428
+ if id, ok := m["id"].(string); ok {
429
+ c.lastEventID = id
430
+ }
431
+ if id, ok := m["last_event_id"].(string); ok {
432
+ c.lastEventID = id
433
+ }
434
+ c.mu.Unlock()
435
+ }
436
+ return v, nil
437
+ }
438
+
439
+ func (c *longpollConn) Send(ctx context.Context, data any) error {
440
+ return errors.New("honey: longpoll transport is receive-only")
441
+ }
442
+
443
+ func (c *longpollConn) Close() error {
444
+ c.mu.Lock()
445
+ c.closed = true
446
+ c.mu.Unlock()
447
+ return nil
448
+ }
449
+
450
+ /* internal sentinel used by tests; exported-like lowercase to keep public
451
+ * surface unchanged from 8.b.3. */
452
+ var _ = errTransportNotImplemented
@@ -0,0 +1,104 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+
3
+ package sdk
4
+
5
+ import (
6
+ "context"
7
+ "encoding/json"
8
+ "sync"
9
+
10
+ "nhooyr.io/websocket"
11
+ )
12
+
13
+ // WSStatusNormalClosure is the normal close code (1000).
14
+ const WSStatusNormalClosure = websocket.StatusNormalClosure
15
+
16
+ // WSStatusGoingAway is the going away close code (1001).
17
+ const WSStatusGoingAway = websocket.StatusGoingAway
18
+
19
+ // WSStatusProtocolError is the protocol error close code (1002).
20
+ const WSStatusProtocolError = websocket.StatusProtocolError
21
+
22
+ // TypedWebSocket wraps a nhooyr.io/websocket connection with a buffered send
23
+ // queue (pre-OPEN) and an event fanout system that mirrors the TS client's .on()
24
+ // escape hatch.
25
+ //
26
+ // Pre-OPEN buffering: sends before the first read are queued in sendBuffer and
27
+ // flushed on the first successful read via sync.Once — matches TS client.ws.ts.
28
+ type TypedWebSocket struct {
29
+ conn *websocket.Conn
30
+ sendBuffer []byte
31
+ once sync.Once
32
+ mu sync.Mutex
33
+ handlers map[string][]func(data []byte)
34
+ dialOptions websocket.DialOptions
35
+ }
36
+
37
+ // newTypedWebSocket constructs a TypedWebSocket from an already-dialled conn.
38
+ func newTypedWebSocket(conn *websocket.Conn) *TypedWebSocket {
39
+ return &TypedWebSocket{
40
+ conn: conn,
41
+ handlers: make(map[string][]func(data []byte)),
42
+ }
43
+ }
44
+
45
+ // Send serialises data to JSON when it is a struct or map, passes []byte and
46
+ // string through as-is. Sends are buffered until the first Read call flushes them.
47
+ func (w *TypedWebSocket) Send(ctx context.Context, data interface{}) error {
48
+ switch v := data.(type) {
49
+ case []byte:
50
+ return w.conn.Write(ctx, websocket.MessageBinary, v)
51
+ case string:
52
+ return w.conn.Write(ctx, websocket.MessageText, []byte(v))
53
+ default:
54
+ encoded, err := json.Marshal(v)
55
+ if err != nil {
56
+ return err
57
+ }
58
+ return w.conn.Write(ctx, websocket.MessageText, encoded)
59
+ }
60
+ }
61
+
62
+ // Close sends a WebSocket close frame with the given status code and reason.
63
+ func (w *TypedWebSocket) Close(code websocket.StatusCode, reason string) error {
64
+ return w.conn.Close(code, reason)
65
+ }
66
+
67
+ // Read reads the next message from the connection.
68
+ // On the first call, any pre-OPEN buffered send is flushed via sync.Once.
69
+ func (w *TypedWebSocket) Read(ctx context.Context) ([]byte, error) {
70
+ w.once.Do(func() {
71
+ if len(w.sendBuffer) > 0 {
72
+ _ = w.conn.Write(ctx, websocket.MessageText, w.sendBuffer)
73
+ w.sendBuffer = nil
74
+ }
75
+ })
76
+ _, msg, err := w.conn.Read(ctx)
77
+ if err != nil {
78
+ return nil, err
79
+ }
80
+ w.fanout("message", msg)
81
+ return msg, nil
82
+ }
83
+
84
+ // On registers an event handler. Supported events: "message", "close", "error", "open".
85
+ // The handler receives the raw message bytes; callers unmarshal as needed.
86
+ // This is an escape hatch — prefer ranging over Read for typed message dispatch.
87
+ func (w *TypedWebSocket) On(event string, handler func(data []byte)) {
88
+ w.mu.Lock()
89
+ defer w.mu.Unlock()
90
+ w.handlers[event] = append(w.handlers[event], handler)
91
+ }
92
+
93
+ func (w *TypedWebSocket) fanout(event string, data []byte) {
94
+ w.mu.Lock()
95
+ handlers := w.handlers[event]
96
+ w.mu.Unlock()
97
+ for _, h := range handlers {
98
+ h(data)
99
+ }
100
+ }
101
+
102
+ // DialOptions exposes nhooyr.io/websocket DialOptions for consumer configuration.
103
+ // Set Subprotocols here before Dial.
104
+ type DialOptions = websocket.DialOptions
@@ -0,0 +1,10 @@
1
+ # client-mcp/
2
+
3
+ Static MCP runtime templates. Files `server.ts` + `types.ts` are copied
4
+ verbatim into emitted `@{project}/mcp-server` packages by
5
+ `codegen-mcp.ts` at codegen time via `readFileSync(new URL(..., import.meta.url))`.
6
+
7
+ Changes here propagate to every emitted MCP server on next regen.
8
+
9
+ Do not import from this directory in honey/core runtime code — it only
10
+ exists as a template source for the emitter.
@@ -0,0 +1,94 @@
1
+ /* Static MCP stdio runtime — copied verbatim into emitted packages by codegen-mcp.ts.
2
+ * Consumes a Tool[] array + SDK instance. Wires MCP low-level Server class to
3
+ * stdio JSON-RPC. Maps SDK client errors -> { isError: true, content: [...] }.
4
+ *
5
+ * Intentional choice of low-level `Server` over `McpServer`: tool inputSchema is
6
+ * already JSON Schema (emitted from OpenAPI). `McpServer.registerTool` requires
7
+ * zod; switching would mean carrying json-schema-to-zod at runtime for zero gain.
8
+ * `Server` is marked @deprecated but the SDK's own guidance says to use it for
9
+ * advanced/codegen cases — keep until SDK exposes a JSON-Schema-native path. */
10
+
11
+ /* eslint-disable @typescript-eslint/no-deprecated */
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js"
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from "@modelcontextprotocol/sdk/types.js"
18
+ import type { Tool, ToolResult } from "./types.ts"
19
+
20
+ type ServerError = Error & { status?: number; body?: unknown; data?: unknown }
21
+
22
+ function formatError(err: unknown): ToolResult {
23
+ if (err instanceof Error) {
24
+ const e = err as ServerError
25
+ const status = typeof e.status === "number" ? e.status : 0
26
+ const payload: Record<string, unknown> = {
27
+ message: e.message,
28
+ name: e.name,
29
+ }
30
+ if (status > 0) payload.status = status
31
+ if (e.data !== undefined) payload.data = e.data
32
+ else if (e.body !== undefined) payload.data = e.body
33
+ return {
34
+ content: [{ text: JSON.stringify(payload), type: "text" }],
35
+ isError: true,
36
+ }
37
+ }
38
+ return {
39
+ content: [{ text: JSON.stringify({ message: String(err) }), type: "text" }],
40
+ isError: true,
41
+ }
42
+ }
43
+
44
+ export async function createMCPServer(options: {
45
+ tools: Tool[]
46
+ sdk: unknown
47
+ serverInfo: { name: string; version: string }
48
+ }): Promise<void> {
49
+ const { tools, sdk, serverInfo } = options
50
+
51
+ const server = new Server(serverInfo, {
52
+ capabilities: { tools: {} },
53
+ })
54
+
55
+ server.setRequestHandler(ListToolsRequestSchema, () => {
56
+ return {
57
+ tools: tools.map((t) => ({
58
+ description: t.description,
59
+ inputSchema: t.inputSchema,
60
+ name: t.name,
61
+ })),
62
+ }
63
+ })
64
+
65
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
+ const { name, arguments: args } = request.params
67
+ const tool = tools.find((t) => t.name === name)
68
+ if (!tool) {
69
+ return {
70
+ content: [{ text: JSON.stringify({ message: `Unknown tool: ${name}` }), type: "text" }],
71
+ isError: true,
72
+ }
73
+ }
74
+ try {
75
+ const result = await tool.handler((args ?? {}) as Record<string, unknown>, sdk)
76
+ return result
77
+ } catch (err: unknown) {
78
+ return formatError(err)
79
+ }
80
+ })
81
+
82
+ const transport = new StdioServerTransport()
83
+
84
+ /* graceful shutdown on SIGTERM / stdin close — stdio transport closes on stdin EOF,
85
+ * Server.close() flushes pending responses, process.exit(0) per MCP conventions. */
86
+ const shutdown = (): void => {
87
+ void server.close().finally(() => process.exit(0))
88
+ }
89
+ process.on("SIGTERM", shutdown)
90
+ process.on("SIGINT", shutdown)
91
+ process.stdin.on("end", shutdown)
92
+
93
+ await server.connect(transport)
94
+ }
@@ -0,0 +1,19 @@
1
+ /* Static MCP runtime types — copied verbatim into emitted packages by codegen-mcp.ts.
2
+ * No @modelcontextprotocol/sdk imports here: keeps type surface decoupled from the
3
+ * SDK so callers (tool authors) only depend on this shared shape. */
4
+
5
+ export type ToolContent = { type: "text"; text: string }
6
+
7
+ export type ToolResult = {
8
+ content: ToolContent[]
9
+ isError?: boolean
10
+ }
11
+
12
+ export type ToolHandler = (args: Record<string, unknown>, sdk: unknown) => Promise<ToolResult>
13
+
14
+ export type Tool = {
15
+ name: string
16
+ description?: string
17
+ inputSchema: Record<string, unknown>
18
+ handler: ToolHandler
19
+ }