Repository navigation
Expand file tree
/
Copy pathai-code-mcp-http-server.el
More file actions
622 lines (571 loc) · 25.6 KB
/
Copy pathai-code-mcp-http-server.el
File metadata and controls
622 lines (571 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
;;; ai-code-mcp-http-server.el --- HTTP transport for ai-code MCP tools -*- lexical-binding: t; -*-
;; Author: Kang Tu <tninja@gmail.com>
;; SPDX-License-Identifier: Apache-2.0
;;; Commentary:
;; Minimal local HTTP transport for `ai-code-mcp-server'.
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'seq)
(require 'subr-x)
(require 'ai-code-mcp-server)
(defgroup ai-code-mcp-http-server nil
"HTTP transport for AI Code MCP tools."
:group 'tools
:prefix "ai-code-mcp-http-server-")
(defcustom ai-code-mcp-http-server-port nil
"Port used by the local MCP HTTP server.
When nil, an available port is selected automatically."
:type '(choice (const :tag "Auto-select" nil)
integer)
:group 'ai-code-mcp-http-server)
(defcustom ai-code-mcp-http-server-max-request-bytes (* 1024 1024)
"Maximum accepted MCP HTTP request body size in bytes."
:type 'natnum
:group 'ai-code-mcp-http-server)
(defcustom ai-code-mcp-http-server-max-header-bytes (* 64 1024)
"Maximum accepted MCP HTTP request header size in bytes."
:type 'natnum
:group 'ai-code-mcp-http-server)
(defcustom ai-code-mcp-http-server-rate-limit 120
"Maximum authenticated requests allowed per session rate window."
:type 'natnum
:group 'ai-code-mcp-http-server)
(defcustom ai-code-mcp-http-server-rate-window-seconds 60
"Length of the authenticated request rate window in seconds."
:type 'positive-integer
:group 'ai-code-mcp-http-server)
(defvar ai-code-mcp-http-server--server nil
"Server process for the local MCP HTTP transport.")
(defvar ai-code-mcp-http-server--port nil
"Port for `ai-code-mcp-http-server--server'.")
(defun ai-code-mcp-http-server-live-p ()
"Return non-nil when the local MCP HTTP server is running."
(and ai-code-mcp-http-server--server
(process-live-p ai-code-mcp-http-server--server)
ai-code-mcp-http-server--port))
(defun ai-code-mcp-http-server-ensure ()
"Start the local MCP HTTP server when needed and return its port."
(unless (ai-code-mcp-http-server-live-p)
(ai-code-mcp-http-server--start))
ai-code-mcp-http-server--port)
(defun ai-code-mcp-http-server-stop ()
"Stop the local MCP HTTP server."
(interactive)
(when (process-live-p ai-code-mcp-http-server--server)
(delete-process ai-code-mcp-http-server--server))
(setq ai-code-mcp-http-server--server nil
ai-code-mcp-http-server--port nil))
(defun ai-code-mcp-http-server--start ()
"Start the local MCP HTTP server."
(let ((server (make-network-process
:name "ai-code-mcp-http-server"
:server t
:host "127.0.0.1"
:service (or ai-code-mcp-http-server-port 0)
:noquery t
:filter #'ai-code-mcp-http-server--filter
:log #'ai-code-mcp-http-server--accept)))
(setq ai-code-mcp-http-server--server server
ai-code-mcp-http-server--port (process-contact server :service))))
(defun ai-code-mcp-http-server--accept (_server client _message)
"Initialize accepted CLIENT process."
(set-process-query-on-exit-flag client nil)
(set-process-buffer client (generate-new-buffer " *ai-code-mcp-http-client*"))
(set-process-coding-system client 'binary 'binary)
(process-put client :data "")
(set-process-filter client #'ai-code-mcp-http-server--filter)
(set-process-sentinel client #'ai-code-mcp-http-server--client-sentinel))
(defun ai-code-mcp-http-server--client-sentinel (process _event)
"Clean up PROCESS buffer after the client disconnects."
(when (and (not (process-live-p process))
(buffer-live-p (process-buffer process)))
(kill-buffer (process-buffer process))))
(defun ai-code-mcp-http-server--filter (process chunk)
"Accumulate CHUNK for PROCESS and handle a full request."
(condition-case nil
(let* ((data (concat (or (process-get process :data) "") chunk))
(header-end (string-match "\r\n\r\n" data)))
(cond
((and (null header-end)
(> (string-bytes data)
ai-code-mcp-http-server-max-header-bytes))
(ai-code-mcp-http-server--send-response
process 431 "text/plain" "Request Header Fields Too Large"))
((and header-end
(> header-end ai-code-mcp-http-server-max-header-bytes))
(ai-code-mcp-http-server--send-response
process 431 "text/plain" "Request Header Fields Too Large"))
((and header-end
(> (string-bytes data)
(+ header-end 4
ai-code-mcp-http-server-max-request-bytes)))
(ai-code-mcp-http-server--send-response
process 413 "text/plain" "Payload Too Large"))
(t
(process-put process :data data)
(when-let ((request
(ai-code-mcp-http-server--parse-request data)))
(process-put process :data nil)
(ai-code-mcp-http-server--handle-request process request)))))
(error
(ai-code-mcp-http-server--send-response
process 400 "text/plain" "Bad Request"))))
(defun ai-code-mcp-http-server--content-length (headers)
"Return the validated Content-Length from HEADERS, or `invalid'."
(let ((values
(mapcar #'cdr
(seq-filter
(lambda (header)
(equal (car header) "content-length"))
headers))))
(cond
((null values) 0)
((not (= (length values) 1)) 'invalid)
((not (string-match-p "\\`[0-9]+\\'" (car values))) 'invalid)
(t (string-to-number (car values))))))
(defun ai-code-mcp-http-server--parse-request (data)
"Parse DATA when it includes a full HTTP request."
(when (string-match "\r\n\r\n" data)
(let* ((separator-start (match-beginning 0))
(body-start (match-end 0))
(header-text (substring data 0 separator-start))
(lines (split-string header-text "\r\n" t))
(request-line (car lines))
(headers (delq nil (mapcar #'ai-code-mcp-http-server--parse-header
(cdr lines))))
(content-length
(ai-code-mcp-http-server--content-length headers)))
(pcase-let ((`(,method ,path)
(ai-code-mcp-http-server--parse-request-line request-line)))
(cond
((eq content-length 'invalid)
(list :method method :path path :headers headers :bad-request t))
((> content-length ai-code-mcp-http-server-max-request-bytes)
(list :method method :path path :headers headers :too-large t))
((<= (+ body-start content-length) (string-bytes data))
(list :method method
:path path
:headers headers
:body (substring data body-start
(+ body-start content-length)))))))))
(defun ai-code-mcp-http-server--parse-request-line (line)
"Parse HTTP request LINE."
(let ((parts (split-string line " " t)))
(unless (>= (length parts) 2)
(error "Malformed request line"))
(list (nth 0 parts) (nth 1 parts))))
(defun ai-code-mcp-http-server--parse-header (line)
"Parse HTTP header LINE into a cons cell."
(when (string-match "\\`\\([^:]+\\):[ \t]*\\(.*\\)\\'" line)
(cons (downcase (match-string 1 line))
(match-string 2 line))))
(defun ai-code-mcp-http-server--handle-request (process request)
"Handle REQUEST received on PROCESS."
(condition-case err
(let ((session (ai-code-mcp-http-server--authenticate request)))
(cond
((plist-get request :bad-request)
(ai-code-mcp-http-server--send-response
process 400 "text/plain" "Bad Request"))
((plist-get request :too-large)
(ai-code-mcp-http-server--send-response
process 413 "text/plain" "Payload Too Large"))
((not (equal (plist-get request :path) "/mcp"))
(ai-code-mcp-http-server--send-response
process 404 "text/plain" "Not Found"))
((not (ai-code-mcp-http-server--origin-allowed-p request))
(ai-code-mcp-http-server--send-response
process 403 "text/plain" "Forbidden"))
((not session)
(ai-code-mcp-http-server--send-response
process 401 "text/plain" ""))
((ai-code-mcp-http-server--rate-limited-p (car session))
(ai-code-mcp-http-server--send-response
process 429 "text/plain" "Too Many Requests"
`(("Retry-After" . ,(number-to-string
ai-code-mcp-http-server-rate-window-seconds)))))
((and (equal (plist-get request :method) "POST")
(not (ai-code-mcp-http-server--json-content-type-p request)))
(ai-code-mcp-http-server--send-response
process 415 "text/plain" "Unsupported Media Type"))
((and (equal (plist-get request :method) "POST")
(not (ai-code-mcp-http-server--accepts-mcp-responses-p request)))
(ai-code-mcp-http-server--send-response
process 406 "text/plain" "Not Acceptable"))
((equal (plist-get request :method) "POST")
(let ((ai-code-mcp--current-session-id (car session)))
(ai-code-mcp-http-server--handle-post process request)))
(t
(ai-code-mcp-http-server--send-response
process 405 "text/plain" "Method Not Allowed"))))
(json-parse-error
(ai-code-mcp-http-server--send-json-error
process nil -32700 "Parse error"
(if (ai-code-mcp-http-server--modern-request-headers-p request)
400
200)))
(ai-code-mcp-protocol-error
(ai-code-mcp-http-server--send-json-error
process
(ai-code-mcp-http-server--request-id request)
(nth 1 err)
(nth 2 err)
200
(nth 3 err)))
(error
(display-warning
'ai-code-mcp-http-server
(format "MCP HTTP request failed: %s" (error-message-string err)))
(ai-code-mcp-http-server--send-json-error
process
(ai-code-mcp-http-server--request-id request)
-32603
"Internal error"))))
(defun ai-code-mcp-http-server--origin-allowed-p (request)
"Return non-nil when REQUEST has no Origin or a loopback Origin."
(let ((origin (cdr (assoc "origin" (plist-get request :headers))))
(case-fold-search t))
(or (null origin)
(string-match-p
"\\`https?://\\(?:localhost\\|127\\.0\\.0\\.1\\|\\[::1\\]\\)\\(?::[0-9]+\\)?\\'"
origin))))
(defun ai-code-mcp-http-server--json-content-type-p (request)
"Return non-nil when REQUEST declares an application/json body."
(let ((content-type
(cdr (assoc "content-type" (plist-get request :headers))))
(case-fold-search t))
(and content-type
(string-match-p
"\\`application/json\\(?:[ ]*;.*\\)?\\'"
content-type))))
(defun ai-code-mcp-http-server--rate-limited-p (session-id)
"Record a request for SESSION-ID and return non-nil when over limit."
(let* ((context (ai-code-mcp-get-session-context session-id))
(now (float-time))
(window-start (plist-get context :rate-window-start))
(count (or (plist-get context :rate-count) 0)))
(when (or (null window-start)
(>= (- now window-start)
ai-code-mcp-http-server-rate-window-seconds))
(setq window-start now
count 0))
(if (>= count ai-code-mcp-http-server-rate-limit)
t
(setq context (plist-put context :rate-window-start window-start))
(setq context (plist-put context :rate-count (1+ count)))
(puthash session-id context ai-code-mcp--sessions)
nil)))
(defun ai-code-mcp-http-server--accepts-mcp-responses-p (request)
"Return non-nil when REQUEST accepts JSON and event-stream responses."
(let* ((accept (cdr (assoc "accept" (plist-get request :headers))))
(types
(mapcar (lambda (value)
(downcase
(string-trim (car (split-string value ";" t)))))
(split-string (or accept "") "," t))))
(and (member "application/json" types)
(member "text/event-stream" types))))
(defun ai-code-mcp-http-server--authenticate (request)
"Return the MCP session authenticated by REQUEST, or nil."
(let* ((authorization
(cdr (assoc "authorization" (plist-get request :headers))))
(case-fold-search t)
(token (and authorization
(string-match "\\`Bearer[ ]+\\(.+\\)\\'" authorization)
(match-string 1 authorization))))
(ai-code-mcp-find-session-by-token token)))
(defun ai-code-mcp-http-server--handle-post (process request)
"Handle POST REQUEST on PROCESS."
(let* ((json-object
(ai-code-mcp-http-server--parse-json
(plist-get request :body)))
(id (alist-get 'id json-object))
(method (alist-get 'method json-object))
(params (alist-get 'params json-object))
(context (ai-code-mcp-get-session-context))
(modern (ai-code-mcp-http-server--modern-request-p
request method params))
(envelope-error
(ai-code-mcp-http-server--json-rpc-envelope-error json-object))
(transport-error
(unless envelope-error
(if modern
(ai-code-mcp-http-server--modern-transport-error
method params request context)
(ai-code-mcp-http-server--legacy-transport-error
method request context)))))
(cond
(envelope-error
(ai-code-mcp-http-server--send-json-error
process id -32600 envelope-error (if modern 400 200)))
(transport-error
(ai-code-mcp-http-server--send-json-error
process id
(or (plist-get transport-error :code) -32600)
(or (plist-get transport-error :message) transport-error)
400
(plist-get transport-error :data)))
((not (ai-code-mcp-http-server--method-allowed-p
method context modern))
(ai-code-mcp-http-server--send-json-error
process id -32600 "MCP session is not ready" 400))
(t
(let* ((transport-session-id
(when (equal method "initialize")
(ai-code-mcp-http-server--begin-legacy-session params)))
(_completed
(when (equal method "notifications/initialized")
(ai-code-mcp-http-server--complete-legacy-session request)))
(response-headers
(when transport-session-id
`(("MCP-Session-Id" . ,transport-session-id)))))
(ai-code-mcp-http-server--dispatch-request
process json-object modern response-headers))))))
(defun ai-code-mcp-http-server--parse-json (body)
"Parse JSON request BODY into the server's canonical representation."
(json-parse-string body
:object-type 'alist
:array-type 'array
:null-object :null
:false-object :json-false))
(defun ai-code-mcp-http-server--modern-request-headers-p (request)
"Return non-nil when REQUEST headers select the modern protocol."
(let ((version (cdr (assoc "mcp-protocol-version"
(plist-get request :headers)))))
(and version
(not (member version ai-code-mcp--legacy-protocol-versions)))))
(defun ai-code-mcp-http-server--modern-request-p (request method params)
"Return non-nil when REQUEST, METHOD, and PARAMS use modern MCP."
(let* ((meta (and (listp params) (alist-get '_meta params)))
(body-version
(and (listp meta)
(alist-get 'io.modelcontextprotocol/protocolVersion meta))))
(or (equal method "server/discover")
(ai-code-mcp-http-server--modern-request-headers-p request)
(equal body-version ai-code-mcp--modern-protocol-version))))
(defun ai-code-mcp-http-server--modern-transport-error
(method params request context)
"Return a modern transport error for METHOD, PARAMS, REQUEST, and CONTEXT."
(let* ((headers (plist-get request :headers))
(header-version (cdr (assoc "mcp-protocol-version" headers)))
(header-method (cdr (assoc "mcp-method" headers)))
(header-name (cdr (assoc "mcp-name" headers)))
(meta (and (listp params) (alist-get '_meta params)))
(version-entry
(and (listp meta)
(assq 'io.modelcontextprotocol/protocolVersion meta)))
(capabilities-entry
(and (listp meta)
(assq 'io.modelcontextprotocol/clientCapabilities meta)))
(body-version (cdr version-entry))
(body-name (and (listp params)
(or (alist-get 'name params)
(alist-get 'uri params))))
(requires-name
(member method '("tools/call" "resources/read" "prompts/get"))))
(cond
((not (ai-code-mcp-http-server--modern-protocol-enabled-p context))
(list :code -32022
:message "Unsupported protocol version"
:data `((supported . [])
(requested . ,(or body-version header-version)))))
((or (null version-entry) (not (stringp body-version))
(null capabilities-entry))
(list :code -32602
:message "Missing required per-request MCP metadata"))
((not (equal header-version body-version))
(list :code -32020
:message "Header mismatch: MCP-Protocol-Version"))
((not (equal body-version ai-code-mcp--modern-protocol-version))
(list :code -32022
:message "Unsupported protocol version"
:data `((supported . [,ai-code-mcp--modern-protocol-version])
(requested . ,body-version))))
((not (equal header-method method))
(list :code -32020
:message "Header mismatch: Mcp-Method"))
((and requires-name
(not (equal (ai-code-mcp-http-server--decode-header-value
header-name)
body-name)))
(list :code -32020
:message "Header mismatch: Mcp-Name")))))
(defun ai-code-mcp-http-server--modern-protocol-enabled-p (context)
"Return whether CONTEXT permits the stateless modern MCP protocol."
(or (not (plist-member context :modern-protocol-enabled))
(plist-get context :modern-protocol-enabled)))
(defun ai-code-mcp-http-server--decode-header-value (value)
"Decode modern MCP header VALUE, including its Base64 sentinel form."
(when value
(if (string-match "\\`=\\?base64\\?\\(.+\\)\\?=\\'" value)
(condition-case nil
(decode-coding-string
(base64-decode-string (match-string 1 value))
'utf-8)
(error nil))
value)))
(defun ai-code-mcp-http-server--dispatch-request
(process json-object modern response-headers)
"Dispatch JSON-OBJECT and reply on PROCESS.
MODERN selects stateless protocol semantics and RESPONSE-HEADERS are appended
to a successful response."
(let* ((id-entry (assq 'id json-object))
(id (cdr id-entry))
(method (alist-get 'method json-object))
(params (alist-get 'params json-object))
(ai-code-mcp--current-protocol-version
(and modern ai-code-mcp--modern-protocol-version)))
(if (null id-entry)
(ai-code-mcp-http-server--send-accepted process)
(condition-case err
(ai-code-mcp-http-server--send-json
process 200
`((jsonrpc . "2.0")
(id . ,id)
(result . ,(ai-code-mcp-dispatch method params)))
response-headers)
(ai-code-mcp-protocol-error
(let ((code (nth 1 err)))
(ai-code-mcp-http-server--send-json-error
process id code (nth 2 err)
(cond
((not modern) 200)
((= code -32601) 404)
(t 400))
(nth 3 err))))
(error
(display-warning
'ai-code-mcp-http-server
(format "MCP dispatch failed: %s" (error-message-string err)))
(ai-code-mcp-http-server--send-json-error
process id -32603 "Internal error" 500))))))
(defun ai-code-mcp-http-server--json-rpc-envelope-error (json-object)
"Return an error string when JSON-OBJECT is not a JSON-RPC request."
(cond
((not (listp json-object)) "Invalid JSON-RPC request object")
((not (equal "2.0" (alist-get 'jsonrpc json-object)))
"Invalid or missing jsonrpc version")
((not (stringp (alist-get 'method json-object)))
"Invalid or missing JSON-RPC method")
((and (assq 'id json-object)
(not (or (stringp (alist-get 'id json-object))
(integerp (alist-get 'id json-object)))))
"JSON-RPC id must be a string or integer")
((and (assq 'params json-object)
(not (listp (alist-get 'params json-object))))
"JSON-RPC params must be an object")))
(defun ai-code-mcp-http-server--legacy-transport-error (method request context)
"Return a legacy transport error for METHOD, REQUEST, and CONTEXT."
(unless (equal method "initialize")
(let* ((headers (plist-get request :headers))
(expected-session-id (plist-get context :transport-session-id))
(actual-session-id (cdr (assoc "mcp-session-id" headers)))
(expected-version (plist-get context :protocol-version))
(actual-version (cdr (assoc "mcp-protocol-version" headers))))
(cond
((and expected-session-id
(not (equal expected-session-id actual-session-id)))
(list :code -32600
:message "Missing or invalid MCP-Session-Id header"))
((and expected-version
(not (equal expected-version actual-version)))
(list :code -32600
:message "Missing or invalid MCP-Protocol-Version header"))))))
(defun ai-code-mcp-http-server--method-allowed-p (method context modern)
"Return non-nil when METHOD is valid for CONTEXT and protocol era MODERN."
(if modern
(or (member method '("server/discover" "tools/list" "ping"))
(not (equal method "tools/call"))
(eq (plist-get context :state) 'ready))
(or (member method '("initialize" "notifications/initialized" "ping"))
(eq (plist-get context :state) 'ready))))
(defun ai-code-mcp-http-server--begin-legacy-session (params)
"Record a legacy MCP session initialized with PARAMS."
(let* ((session-id ai-code-mcp--current-session-id)
(protocol-version
(ai-code-mcp-negotiate-legacy-version
(alist-get 'protocolVersion params)))
(client-info (alist-get 'clientInfo params))
(transport-session-id (ai-code-mcp--random-secret)))
(ai-code-mcp-begin-legacy-session
session-id transport-session-id protocol-version client-info)
transport-session-id))
(defun ai-code-mcp-http-server--complete-legacy-session (request)
"Complete legacy MCP initialization described by REQUEST headers."
(let ((headers (plist-get request :headers)))
(ai-code-mcp-complete-legacy-session
ai-code-mcp--current-session-id
(cdr (assoc "mcp-session-id" headers))
(cdr (assoc "mcp-protocol-version" headers)))))
(defun ai-code-mcp-http-server--request-id (request)
"Extract the JSON-RPC request ID from REQUEST."
(when-let ((body (plist-get request :body)))
(condition-case nil
(alist-get 'id
(ai-code-mcp-http-server--parse-json body))
(error nil))))
(defun ai-code-mcp-http-server--send-json-error
(process id code message &optional http-code data)
"Send a JSON-RPC error on PROCESS.
ID, CODE, MESSAGE, HTTP-CODE, and DATA describe the error response."
(ai-code-mcp-http-server--send-json
process
(or http-code 500)
`((jsonrpc . "2.0")
(id . ,id)
(error . ((code . ,code)
(message . ,message)
,@(when data `((data . ,data))))))))
(defun ai-code-mcp-http-server--send-json (process code payload &optional headers)
"Send JSON PAYLOAD with HTTP CODE and optional HEADERS on PROCESS."
(let ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol))
(ai-code-mcp-http-server--send-response
process
code
"application/json"
(json-encode payload)
headers)))
(defun ai-code-mcp-http-server--send-accepted (process)
"Send an empty HTTP 202 Accepted response on PROCESS."
(ai-code-mcp-http-server--send-response process 202 "text/plain" ""))
(defun ai-code-mcp-http-server--send-response
(process code content-type body &optional headers)
"Send an HTTP response on PROCESS.
CODE, CONTENT-TYPE, BODY, and HEADERS describe the response."
(let* ((payload (or body ""))
(response (concat
(format "HTTP/1.1 %d %s\r\n"
code
(ai-code-mcp-http-server--reason code))
(format "Content-Type: %s\r\n" content-type)
(format "Content-Length: %d\r\n" (string-bytes payload))
(mapconcat (lambda (header)
(format "%s: %s\r\n"
(car header) (cdr header)))
headers
"")
"Connection: close\r\n\r\n"
payload)))
(process-send-string process response)
(delete-process process)))
(defun ai-code-mcp-http-server--reason (code)
"Return the HTTP reason phrase for CODE."
(alist-get code '((200 . "OK")
(202 . "Accepted")
(400 . "Bad Request")
(401 . "Unauthorized")
(403 . "Forbidden")
(404 . "Not Found")
(405 . "Method Not Allowed")
(406 . "Not Acceptable")
(413 . "Payload Too Large")
(415 . "Unsupported Media Type")
(429 . "Too Many Requests")
(431 . "Request Header Fields Too Large")
(500 . "Internal Server Error"))
"OK"))
(provide 'ai-code-mcp-http-server)
;;; ai-code-mcp-http-server.el ends here