@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.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- 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
|
+
}
|