Skip to content

Commit c0636fa

Browse files
feat(mcp): adopt protocol 2026-07-28 (#202)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 95d5881 commit c0636fa

16 files changed

Lines changed: 705 additions & 38 deletions

File tree

‎Dockerfile.odek‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# (the sandbox runs with --network=none by default).
1212
#
1313
# Pins:
14-
# - Go minor version follows go.mod ("go 1.25.12") — bump FROM in lockstep.
14+
# - Go version follows go.mod ("go 1.25.13") — bump FROM in lockstep.
1515
# - golangci-lint matches .github/workflows/test.yml (v2.12.2).
1616
# - govulncheck tracks @latest, exactly like CI.
1717
#
@@ -21,7 +21,7 @@
2121
# bumped past the image's Go instead of silently downloading a toolchain
2222
# the --network=none sandbox could never fetch.
2323

24-
FROM golang:1.25-alpine
24+
FROM golang:1.25.13-alpine
2525

2626
ARG GOLANGCI_LINT_VERSION=v2.12.2
2727

‎GETTING_STARTED.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ odek is a single static Go binary (~11 MB, instant startup). No Python, no Node,
1212
|---|---|
1313
| macOS or Linux | Prebuilt binaries, amd64 & arm64. Windows: build from source with Go |
1414
| A z.ai API key | From the [GLM Coding Plan](https://z.ai/subscribe) subscription or pay-as-you-go. Create/manage keys at [z.ai Open Platform](https://z.ai/manage-apikey/apikey-list) |
15-
| Go ≥ 1.25.12 | *Only* if installing from source. Not needed for prebuilt binaries |
15+
| Go ≥ 1.25.13 | *Only* if installing from source. Not needed for prebuilt binaries |
1616
| Docker | *Optional* — enables the default-on sandbox. Skip it with `ODEK_NO_SANDBOX=1` (see [Step 4](#4-sandbox-docker-optional)) |
1717

1818
---

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
One binary. One loop. Zero frameworks. ReAct (Reasoning + Acting) — think, therefore act.
66

77
```bash
8-
# Install (requires Go ≥ 1.25.12 — see "Build requirements" below)
8+
# Install (requires Go ≥ 1.25.13 — see "Build requirements" below)
99
go install github.com/BackendStack21/odek/cmd/odek@latest
1010

1111
# Use (provider env key — DEEPSEEK_API_KEY for the default provider)
@@ -14,7 +14,7 @@ odek run "How many lines in go.mod?"
1414
# → 3 lines
1515
```
1616

17-
**Build requirements:** Go **1.25.12 or newer**. The `go` directive in `go.mod` pins this floor because earlier 1.25.x toolchains ship reachable standard-library CVEs; CI additionally runs `govulncheck` on every push/PR so new advisories fail the build.
17+
**Build requirements:** Go **1.25.13 or newer**. The `go` directive in `go.mod` pins this floor because earlier 1.25.x toolchains ship reachable standard-library CVEs; CI additionally runs `govulncheck` on every push/PR so new advisories fail the build.
1818

1919
**New here?** The step-by-step install guide — including the GLM 5.3 Flash (z.ai) setup and the optional bodek TUI — lives in [GETTING_STARTED.md](GETTING_STARTED.md).
2020

‎cmd/odek/mcp_e2e_test.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package main
22

33
import (
4+
"bufio"
45
"context"
6+
"io"
57
"os"
68
"os/exec"
79
"path/filepath"
810
"runtime"
911
"strings"
12+
"sync"
13+
"syscall"
1014
"testing"
15+
"time"
1116

1217
"github.com/BackendStack21/odek"
1318
"github.com/BackendStack21/odek/internal/config"
19+
"github.com/BackendStack21/odek/internal/mcp"
1420
"github.com/BackendStack21/odek/internal/mcpclient"
1521
)
1622

@@ -44,6 +50,76 @@ func skipIfNoMCPE2E(t *testing.T) {
4450
}
4551
}
4652

53+
type idleSignalReader struct {
54+
reader io.Reader
55+
once sync.Once
56+
}
57+
58+
func (r *idleSignalReader) Read(p []byte) (int, error) {
59+
r.once.Do(func() {
60+
_, _ = io.WriteString(os.Stdout, "MCP_SIGNAL_READY\n")
61+
})
62+
return r.reader.Read(p)
63+
}
64+
65+
func TestMCPServerProcess_SIGTERMExitsWhileIdle(t *testing.T) {
66+
if os.Getenv("MCP_SIGNAL_HELPER") == "1" {
67+
server := mcp.NewServer("test", nil, &idleSignalReader{reader: os.Stdin}, io.Discard)
68+
_ = server.Run(context.Background())
69+
os.Exit(0)
70+
}
71+
72+
cmd := exec.Command(os.Args[0], "-test.run=^TestMCPServerProcess_SIGTERMExitsWhileIdle$")
73+
cmd.Env = append(os.Environ(), "MCP_SIGNAL_HELPER=1")
74+
stdin, err := cmd.StdinPipe()
75+
if err != nil {
76+
t.Fatalf("stdin pipe: %v", err)
77+
}
78+
defer stdin.Close()
79+
stdout, err := cmd.StdoutPipe()
80+
if err != nil {
81+
t.Fatalf("stdout pipe: %v", err)
82+
}
83+
if err := cmd.Start(); err != nil {
84+
t.Fatalf("start helper: %v", err)
85+
}
86+
87+
started := make(chan bool, 1)
88+
go func() {
89+
scanner := bufio.NewScanner(stdout)
90+
for scanner.Scan() {
91+
if scanner.Text() == "MCP_SIGNAL_READY" {
92+
started <- true
93+
return
94+
}
95+
}
96+
started <- false
97+
}()
98+
select {
99+
case ok := <-started:
100+
if !ok {
101+
t.Fatal("helper exited before entering idle MCP read")
102+
}
103+
case <-time.After(2 * time.Second):
104+
_ = cmd.Process.Kill()
105+
t.Fatal("helper did not start MCP server")
106+
}
107+
108+
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
109+
t.Fatalf("signal helper: %v", err)
110+
}
111+
done := make(chan error, 1)
112+
go func() { done <- cmd.Wait() }()
113+
select {
114+
case <-done:
115+
// Termination by SIGTERM is expected; only bounded exit matters.
116+
case <-time.After(time.Second):
117+
_ = cmd.Process.Kill()
118+
<-done
119+
t.Fatal("idle MCP server did not exit after SIGTERM")
120+
}
121+
}
122+
47123
func TestMCPClientE2E_DiscoverTools(t *testing.T) {
48124
skipIfNoMCPE2E(t)
49125

‎cmd/odek/untrusted.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ type untrustedToolWrapper struct {
379379
func (w *untrustedToolWrapper) Name() string { return w.inner.Name() }
380380
func (w *untrustedToolWrapper) Description() string { return w.inner.Description() }
381381
func (w *untrustedToolWrapper) Schema() any { return w.inner.Schema() }
382+
func (w *untrustedToolWrapper) SetContext(ctx context.Context) {
383+
w.ctxTool.SetContext(ctx)
384+
if contextTool, ok := w.inner.(interface{ SetContext(context.Context) }); ok {
385+
contextTool.SetContext(ctx)
386+
}
387+
}
382388
func (w *untrustedToolWrapper) Call(args string) (string, error) {
383389
ctx := w.toolCtx()
384390
out, err := w.inner.Call(args)

‎cmd/odek/untrusted_test.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,15 @@ func TestWrapUntrusted_GuardScansMiddleOfLargeOutput(t *testing.T) {
331331
// untrustedToolWrapper.
332332
type fakeToolForWrapper struct {
333333
name string
334+
ctx context.Context
334335
}
335336

336337
func (f *fakeToolForWrapper) Name() string { return f.name }
337338
func (f *fakeToolForWrapper) Description() string { return "desc" }
338339
func (f *fakeToolForWrapper) Schema() any {
339340
return map[string]any{"type": "object", "properties": map[string]any{}}
340341
}
342+
func (f *fakeToolForWrapper) SetContext(ctx context.Context) { f.ctx = ctx }
341343
func (f *fakeToolForWrapper) Call(args string) (string, error) { return "tool output", nil }
342344

343345
// TestUntrustedToolWrapper_RecordsIngest verifies that the MCP tool wrapper
@@ -349,8 +351,12 @@ func TestUntrustedToolWrapper_RecordsIngest(t *testing.T) {
349351
gotContent = content
350352
})
351353

352-
w := &untrustedToolWrapper{inner: &fakeToolForWrapper{name: "mcp:example"}, source: "mcp:server:tool"}
354+
inner := &fakeToolForWrapper{name: "mcp:example"}
355+
w := &untrustedToolWrapper{inner: inner, source: "mcp:server:tool"}
353356
w.SetContext(ctx)
357+
if inner.ctx != ctx {
358+
t.Fatal("wrapper did not propagate context to inner tool")
359+
}
354360
out, err := w.Call(`{}`)
355361
if err != nil {
356362
t.Fatalf("Call error: %v", err)

‎docs/DEVELOPMENT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ no maintained changelog file — create a release/tag and GitHub produces the no
88

99
## Prerequisites
1010

11-
- Go 1.25.12+ (matches `go.mod`; CI builds with the same toolchain line and runs `govulncheck` on every push/PR)
11+
- Go 1.25.13+ (matches `go.mod`; CI builds with the same toolchain line and runs `govulncheck` on every push/PR)
1212
- Docker (for sandbox integration tests only)
1313

1414
## Building

‎docs/MCP.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,24 @@ fallback applies (default: deny). Configure per-class overrides in `odek.json`:
8080

8181
Stdio transport with JSON-RPC 2.0:
8282

83-
- `initialize` — protocol handshake (`protocolVersion: "2025-03-26"`)
83+
- `server/discover` — stateless protocol and capability discovery
84+
- `initialize` — backward-compatible handshake for `2024-11-05`,
85+
`2025-03-26`, `2025-11-25`, and `2026-07-28` clients
8486
- `tools/list` — returns all available tools with schemas
8587
- `tools/call` — invokes a tool with the given arguments
88+
- `resources/templates/list` — returns an empty compatibility catalog
8689
- `ping` — health check (returns empty object)
8790
- `initialized` — notification
8891

92+
The default protocol is `2026-07-28`. Modern clients declare that version in
93+
each request's `_meta`; responses then include current result and cache
94+
metadata. Legacy clients retain their previous response shapes. Requests are
95+
bounded to 10 MiB by default, malformed JSON-RPC requests are rejected, and
96+
tool-handler panics are contained without exposing the panic value to the
97+
client or stderr. A stateless `tools/call` must include both the modern
98+
protocol version and client capabilities in `_meta`; metadata-free calls are
99+
accepted only after a successful legacy `initialize`.
100+
89101
Logging goes to stderr; stdin/stdout are reserved for the MCP protocol.
90102

91103
---
@@ -94,6 +106,8 @@ Logging goes to stderr; stdin/stdout are reserved for the MCP protocol.
94106

95107
odek can connect to **external MCP servers** and expose their tools to the agent
96108
during `odek run`, `odek repl`, `odek serve`, and `odek mcp`.
109+
The outbound client currently negotiates `2025-03-26` for compatibility with
110+
the versioned [`odek-extension/v1`](EXTENSIONS.md) contract.
97111

98112
### Configuration
99113

‎go.mod‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
module github.com/BackendStack21/odek
22

3-
go 1.25.12
3+
go 1.25.13
44

55
require (
6-
github.com/BackendStack21/go-mcp v1.2.1
7-
github.com/BackendStack21/go-vector v1.3.0
6+
github.com/BackendStack21/go-mcp v1.3.0
7+
github.com/BackendStack21/go-vector v1.4.0
88
golang.org/x/net v0.58.0
99
golang.org/x/term v0.45.0
1010
)
1111

1212
require golang.org/x/sys v0.47.0
1313

14-
require github.com/BackendStack21/go-llm-sdk v0.2.1
14+
require github.com/BackendStack21/go-llm-sdk v0.3.0

‎go.sum‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
github.com/BackendStack21/go-llm-sdk v0.2.1 h1:fnWEYGhh+vi+nN9jjXA/87L1rxBgg9ei4gf6xOyEv3M=
2-
github.com/BackendStack21/go-llm-sdk v0.2.1/go.mod h1:Nhro6plQaVIIFajPhzp2dzz4rv4DFU/yXNEudChllyE=
3-
github.com/BackendStack21/go-mcp v1.2.1 h1:KayKcmOQF5BhhseXEZv7sLqjS3bRRWezOUFosp+P7M4=
4-
github.com/BackendStack21/go-mcp v1.2.1/go.mod h1:RKFw6nrl6ySQqqrR8KtG7HYZ/heyyjT8SjiEtlbTMY8=
5-
github.com/BackendStack21/go-vector v1.3.0 h1:VT1cwPAUzkg3Rt0fXA+jTzW472jgObqs85/TcPs4N7Q=
6-
github.com/BackendStack21/go-vector v1.3.0/go.mod h1:TkxZEqKGeN38QNWUoQt4xJcf6n2kI1pY7nu6ibXK1Yo=
1+
github.com/BackendStack21/go-llm-sdk v0.3.0 h1:QMFKUiIGWZ1PP17fFp+PFZpdYYlWXuBDsOpz0TW8SwI=
2+
github.com/BackendStack21/go-llm-sdk v0.3.0/go.mod h1:Nhro6plQaVIIFajPhzp2dzz4rv4DFU/yXNEudChllyE=
3+
github.com/BackendStack21/go-mcp v1.3.0 h1:55iJgiAgcptv+mQwWEHNaFqPJksPR6zn4beYjHk/Ea4=
4+
github.com/BackendStack21/go-mcp v1.3.0/go.mod h1:RKFw6nrl6ySQqqrR8KtG7HYZ/heyyjT8SjiEtlbTMY8=
5+
github.com/BackendStack21/go-vector v1.4.0 h1:Ssu5RDEt4Cr9A17/ogdfjeuaLFFNHASJBIEGpqKzAV0=
6+
github.com/BackendStack21/go-vector v1.4.0/go.mod h1:TkxZEqKGeN38QNWUoQt4xJcf6n2kI1pY7nu6ibXK1Yo=
77
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
88
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
99
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=

0 commit comments

Comments
 (0)