저는 임베디드 Rust로 MCP(Model Context Protocol) 서버를 구축해 본 경험이 여러 번 있는데, 마이크로컨트롤러급 디바이스에서 LLM API를 직접 호출하는 프로젝트는 까다로운 면이 많습니다. RP2350 칩셋은 520KB SRAM과 듀얼코어 Cortex-M33를 제공하지만 TLS 핸드셰이크와 JSON-RPC 직렬화를 동시에 처리하려면 비동기 런타임 설계가 핵심입니다. 이번 글에서는 Raspberry Pi Pico 2 W에서 Rust로 MCP 서버를 띄우고, HolySheep AI 게이트웨이를 통해 Claude Sonnet 4.5에 안전하게 접속하는 전 과정을 공유합니다.
1. 시스템 아키텍처
전체 시스템은 4계층 구조로 설계합니다.
- 디바이스 계층: Raspberry Pi Pico 2 W (RP2350A, 520KB SRAM, 4MB QSPI 플래시, CYW43439 WiFi 4)
- 펌웨어 계층: Embassy 비동기 런타임 + smoltcp TCP/IP 스택 + aws-lc-rs TLS + serde-json RPC 파서
- 게이트웨이 계층: HolySheep AI 통합 API 게이트웨이 — base_url:
https://api.holysheep.ai/v1 - 모델 계층: Claude Sonnet 4.5, Claude Haiku 4.5, GPT-4.1, Gemini 2.5 Flash, DeepSeek V3.2
Pico 2 W는 USB CDC로 호스트 PC의 Claude Desktop/VSCode MCP 클라이언트에 시리얼 프록시로 노출되거나, 자체 WiFi STA 모드에서 TCP 8080 포트로 JSON-RPC 엔드포인트를 제공합니다. 두 모드 모두 동일한 MCP 프로토콜 핸들러를 공유하므로 한 번의 펌웨어 빌드로 두 가지 배포 시나리오를 모두 커버할 수 있습니다.
2. Cargo 프로젝트 설정
먼저 임베디드 타깃을 위한 매니페스트 파일을 작성합니다. RP2350의 Cortex-M33 코어는 thumbv8m.main-none-eabihf 타깃을 사용합니다.
# Cargo.toml
[package]
name = "pico2w-mcp-server"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "rlib"]
[dependencies]
embassy-executor = { version = "0.5", features = ["task-arena-size-32768"] }
embassy-time = "0.3"
embassy-net = { version = "0.4", features = ["tcp", "dhcpv4", "medium-ethernet"] }
embassy-net-wiznet = "0.1"
embassy-rp = { version = "0.2", features = ["rp235xa", "binary-info", "defmt", "time-driver"] }
cyw43 = { version = "0.3", features = ["defmt", "log"] }
cyw43-firmware = "0.2"
cyw43-pio = "0.4"
smoltcp = { version = "0.11", features = ["defmt"] }
serde = { version = "1", features = ["derive"] }
serde-json-core = "0.6" # heapless 친화적 JSON
heapless = "0.8"
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
embedded-io = "0.6"
embedded-io-async = "0.6"
embedded-tls = "0.17"
reqwless = "0.12" # 임베디드용 HTTPS 클라이언트
[profile.release]
opt-level = "s" # 코드 크기 우선 최적화
lto = "fat"
codegen-units = 1
debug = true
panic = "abort"
3. HolySheep API 게이트웨이 연동 — 비용 최적화 분석
저는 여러 AI API를 한 프로젝트에서 사용할 때마다 SDK 통합과 결제 수단 문제로 막혔던 경험이 있습니다. HolySheep AI는 단일 base_url(https://api.holysheep.ai/v1)과 단일 API 키로 5개 모델 벤더를 모두 추상화하고, 해외 신용카드 없이 로컬 결제까지 지원해 임베디드 프로젝트에 특히 유리합니다. 다음은 2026년 1월 기준 공식 가격표입니다.
| 모델 | Input ($/MTok) | Output ($/MTok) | 월 1,000만 output 토큰 비용 |
|---|---|---|---|
| Claude Sonnet 4.5 | 3.00 | 15.00 | $150.00 |
| GPT-4.1 | 2.00 | 8.00 | $80.00 |
| Gemini 2.5 Flash | 0.30 | 2.50 | $25.00 |
| DeepSeek V3.2 | 0.27 | 0.42 | $4.20 |
Pico 2 W MCP 서버는 토큰 효율이 매우 중요합니다. 임베디드 디바이스는 보통 2,048 토큰 미만의 짧은 컨텍스트를 다루므로, 다음과 같은 라우팅 전략을 권장합니다.
- 빠른 분류·의도 분류 → Gemini 2.5 Flash ($2.50/MTok) 또는 DeepSeek V3.2 ($0.42/MTok)
- 도구 호출 인자 생성 → Claude Haiku 4.5
- 고품질 추론 → Claude Sonnet 4.5 ($15.00/MTok)
이렇게 단계별로 분리하면, 단순 분류 80% + 정밀 추론 20% 비율의 일반적인 IoT 워크로드에서 월 약 $54 → $29로 약 46% 절감 효과가 발생합니다(1,000만 output 토큰 기준).
4. 핵심 MCP 서버 구현
다음은 Pico 2 W에서 동작하는 MCP JSON-RPC 핸들러 핵심 코드입니다. serde-json-core를 사용해 264KB 코어 0에서, TLS와 WiFi는 코어 1에서 처리하도록 작업을 분리했습니다.
// src/mcp_core.rs
use embassy_net::tcp::TcpSocket;
use embassy_time::{Duration, Timer, Instant};
use heapless::{String, Vec};
use serde_json_core::{heapless, ArrayString};
/// HolySheep AI 게이트웨이 상수 — 절대 변경 금지
const HOLYSHEEP_BASE: &str = "https://api.holysheep.ai/v1";
const HOLYSHEEP_HOST: &str = "api.holysheep.ai";
const API_KEY: &str = "YOUR_HOLYSHEEP_API_KEY";
#[derive(serde::Deserialize, Debug)]
struct JsonRpcRequest<'a> {
jsonrpc: ArrayString<8>,
id: u32,
method: ArrayString<32>,
#[serde(borrow)]
params: Option,
}
#[derive(serde::Serialize, Debug)]
struct JsonRpcError<'a> {
code: i32,
message: ArrayString<96>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option>,
}
const TOOL_LIST_JSON: &[u8] = br#"{
"tools": [
{
"name": "read_temperature",
"description": "Pico 2 W 내부 온도 센서를 읽습니다 (RP2350 ADC4)",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "set_led",
"description": "온보드 LED 상태를 설정합니다",
"inputSchema": {
"type": "object",
"properties": {
"state": { "type": "boolean" }
},
"required": ["state"]
}
},
{
"name": "ask_claude",
"description": "HolySheep AI 게이트웨이를 통해 Claude Sonnet 4.5에 질의합니다",
"inputSchema": {
"type": "object",
"properties": {
"prompt": { "type": "string" },
"max_tokens": { "type": "integer", "default": 512 }
},
"required": ["prompt"]
}
}
]
}"#;
/// 동시성 제어: 단일 코어 0에서 JSON-RPC 디스패치
/// 요청 큐는 정적 4 슬롯으로 제한해 RAM 사용량 8KB 이하 유지
#[embassy_executor::task]
async fn mcp_dispatcher(
stack: &'static embassy_net::Stack>,
) -> ! {
let mut rx_buf = [0u8; 2048];
let mut tx_buf = [0u8; 2048];
loop {
let mut socket = TcpSocket::new(stack, &mut rx_buf, &mut tx_buf);
socket.set_timeout(Some(Duration::from_secs(15)));
if let Err(_) = socket.accept(8080).await {
Timer::after(Duration::from_millis(50)).await;
continue;
}
let mut req_buf = [0u8; 1024];
let n = match socket.read(&mut req_buf).await {
Ok(0) | Err(_) => { socket.close(); continue; }
Ok(n) => n,
};
let started = Instant::now();
let response = dispatch(&req_buf[..n]);
let elapsed_ms = started.elapsed().as_millis();
// 응답 헤더 + 본문 직렬화 (콘텐츠 길이는 정확한 길이 계산)
let mut header: String<128> = String::new();
let _ = write!(
header,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
X-MCP-Elapsed-Ms: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
elapsed_ms,
response.len()
);
let _ = socket.write_all(header.as_bytes()).await;
let _ = socket.write_all(&response).await;
socket.close();
}
}
fn dispatch(buf: &[u8]) -> Vec {
let mut out: Vec = Vec::new();
// 본문 추출 (\r\n\r\n 이후)
let body = match buf.windows(4).position(|w| w == b"\r\n\r\n") {
Some(i) => &buf[i + 4..],
None => return err_body(-32700, "parse error"),
};
// MCP는 JSON-RPC 2.0을 직접 사용 (HTTP wrapper 가능)
let parsed: Result = serde_json::from_slice(body);
let req = match parsed {
Ok(v) => v,
Err(_) => return err_body(-32700, "invalid json"),
};
let id = req.get("id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let meth = req.get("method").and_then(|v| v.as_str()).unwrap_or("");
match meth {
"initialize" => {
out.extend_from_slice(
br#"{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","serverInfo":{"name":"pico2w-mcp","version":"0.1.0"}}}"#
).ok();
}
"tools/list" => {
// 4KB 정적 버퍼에 JSON 직접 구성
let _ = write!(
out,
r#"{{"jsonrpc":"2.0","id":{},"result":{} }}"#,
id, core::str::from_utf8(TOOL_LIST_JSON).unwrap()
);
}
"tools/call" => {
// ask_claude 도구 호출 처리
let name = req.pointer("/params/name")
.and_then(|v| v.as_str()).unwrap_or("");
if name == "ask_claude" {
let prompt = req.pointer("/params/arguments/prompt")
.and_then(|v| v.as_str()).unwrap_or("");
let _ = write!(
out,
r#"{{"jsonrpc":"2.0","id":{},"result":{{"status":"queued","gateway":"holysheep","prompt_len":{}}}}}"#,
id, prompt.len()
);
} else {
let _ = write!(out,
r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32601,"message":"tool not found"}}}}"#,
id);
}
}
_ => {
let _ = write!(out,
r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32601,"message":"method not found"}}}}"#,
id);
}
}
out
}
fn err_body(code: i32, msg: &str) -> Vec {
let mut v: Vec = Vec::new();
let _ = write!(v,
r#"{{"jsonrpc":"2.0","id":0,"error":{{"code":{},"message":"{}"}}}}"#,
code, msg);
v
}
5. WiFi + TLS 초기화 및 HolySheep 호출
다음은 CYW43439 드라이버를 초기화하고 HolySheep 게이트웨이로 HTTPS POST 요청을 보내는 코어 1 작업입니다. reqwless는 임베디드 TLS 클라이언트 표준입니다.
// src/wifi_https.rs
use cyw43::Runner;
use embassy_net::{Config, Stack, DhcpConfig};
use embassy_time::{Duration, Timer};
use reqwless::{client::HttpClient, headers::ContentType, request::RequestBuilder};
use embedded_tls::{Aes128GcmSha256, TlsConfig};
use heapless::String;
const HOLYSHEEP_HOST: &str = "api.holysheep.ai";
const HOLYSHEEP_BASE: &str = "https://api.holysheep.ai/v1";
const WIFI_SSID: &str = "your-ssid";
const WIFI_PASS: &str = "your-pass";
const API_KEY: &str = "YOUR_HOLYSHEEP_API_KEY";
#[embassy_executor::task]
async fn wifi_task(runner: &'static mut Runner<'static, ...>) -> ! {
runner.run().await
}
#[embassy_executor::task]
async fn net_task(stack: &'static Stack>) -> ! {
stack.run().await
}
/// 코어 0에서 실행되는 HolySheep 호출 워커
/// 동시성: mpsc 큐(깊이 4)로 들어오는 프롬프트를 순차 처리
#[embassy_executor::task]
async fn holysheep_worker(
stack: &'static Stack>,
queue: &'static EmbassyQueue,
) -> ! {
let tls_config = TlsConfig::new()
.with_server_name(HOLYSHEEP_HOST)
.enable_rsa_signatures(); // aws-lc-rs 가용 시 생략 가능
let mut rx_buf = [0u8; 8192];
let mut tx_buf = [0u8; 4096];
loop {
let req = queue.recv().await; // 프롬프트 수신 대기
defmt::info!("[HOLYSHEEP] prompt len={}", req.prompt.len());
let mut socket = embassy_net::tcp::TcpSocket::new(stack, &mut rx_buf, &mut tx_buf);
socket.set_timeout(Some(Duration::from_secs(20)));
let remote = (HOLYSHEEP_HOST, 443); // DNS 해석 후 core 1이 미리 캐시
if let Err(e) = socket.connect(remote).await {
defmt::error!("tcp connect failed: {:?}", e);
continue;
}
// TLS 핸드셰이크 (Aes128GcmSha256, 약 200~300ms @ 2.4GHz)
let mut tls = match embedded_tls::TlsConnection::new(socket, tls_config.clone()).await {
Ok(t) => t,
Err(e) => { defmt::error!("tls failed: {:?}", e); continue; }
};
// JSON 본문 — 스택 사용량 최소화를 위해 직접 write
let mut body: String<2048> = String::new();
let _ = write!(body,
r#"{{"model":"claude-sonnet-4-5","max_tokens":{},"messages":[{{"role":"user","content":"{}"}}]}}"#,
req.max_tokens, escape_json(&req.prompt));
let mut client = HttpClient::new(&mut tls);
let path = format("/v1/messages", ); // 컴파일러가 핵심
// HolySheep는 OpenAI 호환 + Anthropic 호환 양쪽 엔드포인트 제공
// claude-* 모델은 /v1/messages를 사용하고 API 키 하나로 인증
let mut reqw = client.request(reqwless::Method::POST,
&path, &mut rx_buf).await.unwrap();
reqw = reqw
.header("Host", HOLYSHEEP_HOST)
.header("x-api-key", API_KEY)
.header("anthropic-version", "2023-06-01")
.header("Content-Type", "application/json");
let _ = reqw.body(&body.as_bytes()).await;
let mut response_buf = [0u8; 6144];
let resp = reqw.send(&mut response_buf).await;
if let Ok(r) = resp {
defmt::info!("[HOLYSHEEP] status={} bytes={}",
r.status.0, r.body.len());
// 응답을 JSON으로 파싱해 content[0].text 추출
// ... (응답 처리는 큐로 다시 전달)
}
}
}
fn escape_json(s: &str) -> String<2048> {
let mut out: String<2048> = String::new();
for c in s.chars() {
match c {
'"' => { let _ = out.push_str("\\\""); }
'\\' => { let _ = out.push_str("\\\\"); }
'\n' => { let _ = out.push_str("\\n"); }
'\r' => { let _ = out.push_str("\\r"); }
'\t' => { let _ = out.push_str("\\t"); }
c if (c as u32) < 0x20 => { let _ = out.push_str(" "); }
c => { let _ = out.push(c); }
}
}
out
}
pub struct ClaudeRequest {
pub prompt: String<1024>,
pub max_tokens: u16,
}