cMCP 0.4.1
Model Context Protocol library in pure C11
Loading...
Searching...
No Matches
cmcp_transport.h
Go to the documentation of this file.
1/**
2 * @file cmcp_transport.h
3 * @brief Transport vtable + stdio/HTTP constructors.
4 *
5 * Plugin layer: a `cmcp_transport_t` is a `(read, write, close,
6 * optional wake)` vtable plus a void* context. cMCP ships three
7 * backends:
8 *
9 * - **stdio** (`cmcp_transport_stdio_new` / `_new_fds`): newline-
10 * delimited JSON over stdin/stdout (or any pre-opened fd pair).
11 * - **HTTP server** (`cmcp_transport_http_listen`): Streamable HTTP
12 * hand-rolled on top of `socket()` / `accept()`.
13 * - **HTTP client** (`cmcp_transport_http_connect`): Streamable
14 * HTTP via libcurl with a background SSE reader thread.
15 *
16 * Servers and clients borrow a transport; the *caller* still closes
17 * it. The transport layer is intentionally message-agnostic, with
18 * one principled exception in the HTTP backend (which has to peek
19 * at the JSON-RPC body to distinguish requests-needing-a-response
20 * from notifications-getting-202-Accepted).
21 */
22#ifndef CMCP_TRANSPORT_H
23#define CMCP_TRANSPORT_H
24
25#include <stddef.h>
26
27/* ====================================================================== */
28/* Transport vtable */
29/* ====================================================================== */
30/* A cmcp_transport_t moves opaque byte frames between two MCP peers. It
31 * never inspects message contents — that's the RPC layer's job. Frames
32 * are 1:1 with JSON-RPC messages: one read() returns one whole message,
33 * one write() emits one whole message.
34 *
35 * Concrete transports (stdio, http) plug in by populating the vtable
36 * fields and storing impl-private state in `impl`. */
37
38typedef struct cmcp_transport cmcp_transport_t;
39
41 /* Block until one full frame is available.
42 *
43 * On success: returns CMCP_OK; *out_buf is a freshly malloc'd
44 * NUL-terminated buffer (caller frees with free()), *out_len is its
45 * length in bytes (excluding the NUL).
46 *
47 * On clean EOF or any read failure: returns CMCP_EIO. The caller
48 * should treat this as "the conversation is over." */
49 int (*read_fn)(cmcp_transport_t *t, char **out_buf, size_t *out_len);
50
51 /* Atomically emit one full frame. Adds whatever framing the
52 * transport requires (e.g. trailing newline for stdio). Never
53 * writes partially; safe to call concurrently from multiple
54 * threads (each transport guards writes with its own mutex).
55 *
56 * Returns CMCP_OK on success, CMCP_EIO on write failure,
57 * CMCP_EINVAL if buf contains bytes that would break framing
58 * (e.g. embedded '\n' for stdio). */
59 int (*write_fn)(cmcp_transport_t *t, const char *buf, size_t len);
60
61 /* Tear down the transport: close any owned file descriptors,
62 * release the impl state, free `t` itself. Safe to call with
63 * NULL. After close, t is invalid.
64 *
65 * close_fn must NOT be called while another thread is inside
66 * read_fn. Upper layers that run a reader thread should call
67 * wake_fn (or pthread_kill, for transports that block on a
68 * syscall returning EINTR) to wake the reader, join, then close. */
69 void (*close_fn)(cmcp_transport_t *t);
70
71 /* Optional. Wake any thread blocked inside read_fn so it returns
72 * CMCP_EIO. Idempotent and safe to call multiple times. Does NOT
73 * free anything — close_fn still has to be called separately.
74 *
75 * Required for transports whose read_fn blocks on a userspace
76 * primitive (condvar, futex, etc.) that doesn't surface signals.
77 * Stdio leaves this NULL — its read_fn blocks on a read syscall
78 * which the upper layer interrupts via pthread_kill. */
79 void (*wake_fn)(cmcp_transport_t *t);
80
81 /* Optional. Look up a header from the request currently being
82 * handled, case-insensitively, returning a borrowed pointer (valid
83 * only until the next frame is read) or NULL if absent. Lets a
84 * handler reach transport-level metadata it otherwise couldn't —
85 * e.g. an HTTP `Authorization` value so a host can implement
86 * per-tool auth. Only meaningful for request/response transports
87 * that carry headers; stdio leaves this NULL (always "no header").
88 *
89 * Safe because the HTTP transport handles exactly one request at a
90 * time (single acceptor, single-slot handoff), so "the current
91 * request" is unambiguous for the duration of a handler call. */
92 const char *(*request_header_fn)(cmcp_transport_t *t, const char *name);
93
94 void *impl;
95};
96
97/* Convenience wrappers — exactly equivalent to dispatching through the
98 * vtable. Use these from upper layers so call sites read naturally. */
99
100static inline int cmcp_transport_read(cmcp_transport_t *t,
101 char **out_buf, size_t *out_len) {
102 return t->read_fn(t, out_buf, out_len);
103}
104
105static inline int cmcp_transport_write(cmcp_transport_t *t,
106 const char *buf, size_t len) {
107 return t->write_fn(t, buf, len);
108}
109
110static inline void cmcp_transport_close(cmcp_transport_t *t) {
111 if (t) t->close_fn(t);
112}
113
114static inline void cmcp_transport_wake(cmcp_transport_t *t) {
115 if (t && t->wake_fn) t->wake_fn(t);
116}
117
118static inline const char *cmcp_transport_request_header(cmcp_transport_t *t,
119 const char *name) {
120 if (t && t->request_header_fn) return t->request_header_fn(t, name);
121 return NULL;
122}
123
124/* ====================================================================== */
125/* stdio transport (newline-delimited JSON) */
126/* ====================================================================== */
127/* Frames are JSON messages followed by a single '\n'. The transport
128 * refuses writes containing raw newlines so framing can never desync.
129 *
130 * IMPORTANT: when this transport owns the process's stdin/stdout, NO
131 * other code in the program may write to stdout — every byte must be
132 * a wire frame from this transport. Use stderr for logging. */
133
134/* Open a transport over the calling process's stdin / stdout. Does NOT
135 * close stdin/stdout on close(). Most common case: a server invoked
136 * by a host that pipes to it. */
137cmcp_transport_t *cmcp_transport_stdio_new(void);
138
139/* Open a transport over the given read/write file descriptors. Takes
140 * ownership: close() will close both FDs. Used for spawning children
141 * (read = child's stdout, write = child's stdin) and for tests. */
142cmcp_transport_t *cmcp_transport_stdio_new_fds(int read_fd, int write_fd);
143
144/* ====================================================================== */
145/* Streamable HTTP transport — server side */
146/* ====================================================================== */
147/* Streamable HTTP per MCP spec 2025-11-25: a single `/mcp` endpoint that
148 * accepts POST (request → response) and GET with `Accept:
149 * text/event-stream` (SSE upgrade for server-to-client streams). Session
150 * is identified by the `Mcp-Session-Id` header — minted on the first
151 * `initialize` POST and required on every subsequent request.
152 *
153 * Threading: the transport owns one acceptor thread that loops
154 * accept(). Each connection is handled inline on the acceptor (POSTs
155 * are 1-shot and serialized; SSE connections detach to a background
156 * holder thread). read_fn blocks until a POST body arrives; write_fn
157 * pairs with the most recent unanswered request and unblocks the
158 * acceptor's POST handler so it can send the HTTP response.
159 *
160 * v0.2 limits: one logical session per transport, no TLS (deploy
161 * behind nginx/caddy), no HTTP keep-alive (one request per
162 * connection), Content-Length only (no chunked transfer encoding).
163 *
164 * `host` selects the bind address. A NULL or empty `host` binds the
165 * loopback interface (127.0.0.1) — a safe default that is unreachable
166 * off-box. Pass an explicit address (e.g. "0.0.0.0" or "::") to listen
167 * on a public interface; if you do so without setting
168 * CMCP_HTTP_ALLOWED_ORIGINS, the transport emits a one-shot stderr
169 * warning, since a non-loopback bind with no Origin allow-list has no
170 * DNS-rebinding defense. The transport owns a single listen fd and binds
171 * the first address `getaddrinfo` returns for `host`, so prefer a literal
172 * address over a name that resolves to several families.
173 *
174 * Returns NULL on bind/listen failure (e.g. port in use, permission
175 * denied). The transport begins accepting immediately. */
176cmcp_transport_t *cmcp_transport_http_listen(const char *host,
177 unsigned short port);
178
179/* ====================================================================== */
180/* Streamable HTTP transport — client side */
181/* ====================================================================== */
182/* Connect to a Streamable HTTP MCP server. `url` is the full /mcp
183 * endpoint, e.g. "http://127.0.0.1:8080/mcp" or
184 * "https://example.com/api/mcp" (TLS handled by libcurl).
185 *
186 * Construction is cheap — no network I/O happens until the first
187 * write_fn fires, which POSTs the frame and synchronously waits for
188 * the HTTP response. The transport runs a background SSE reader
189 * thread: once a session id has been latched (from the first
190 * `initialize` response), it opens a long-lived `GET /mcp` with
191 * `Accept: text/event-stream` and feeds each `data: <json>\n\n`
192 * block back through `read_fn` as a frame.
193 *
194 * Frames returned by read_fn come from two sources merged by a
195 * thread-safe queue: synchronous POST responses (HTTP 200 + body)
196 * and SSE-streamed server-pushed messages. Notification POSTs
197 * (HTTP 202 Accepted) carry no body and don't enqueue.
198 *
199 * v0.2 limits: no auto-reconnect on SSE drop; TLS verification uses
200 * the system CA bundle (no custom CA pinning); no exposed knobs for
201 * proxies (set CURL env vars if you need them). */
202cmcp_transport_t *cmcp_transport_http_connect(const char *url);
203
204#endif
cmcp_transport_t * cmcp_transport_stdio_new_fds(int read_fd, int write_fd)
cmcp_transport_t * cmcp_transport_http_connect(const char *url)
cmcp_transport_t * cmcp_transport_stdio_new(void)
cmcp_transport_t * cmcp_transport_http_listen(const char *host, unsigned short port)
void(* close_fn)(cmcp_transport_t *t)
int(* read_fn)(cmcp_transport_t *t, char **out_buf, size_t *out_len)
void(* wake_fn)(cmcp_transport_t *t)
int(* write_fn)(cmcp_transport_t *t, const char *buf, size_t len)