mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-03 15:49:23 +02:00
fix(mcp): address SDK migration review
Co-Authored-By: OpenAI Codex (GPT-5) <noreply@openai.com>
This commit is contained in:
+11
-2
@@ -41,6 +41,9 @@ const maxRequestBodyBytes = 32 << 20
|
|||||||
// session, and initialize takes no token. Clients re-initialize on the 404.
|
// session, and initialize takes no token. Clients re-initialize on the 404.
|
||||||
const sessionTimeout = 30 * time.Minute
|
const sessionTimeout = 30 * time.Minute
|
||||||
|
|
||||||
|
// httpReadHeaderTimeout bounds slow header reads without limiting SSE writes.
|
||||||
|
const httpReadHeaderTimeout = 10 * time.Second
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mcpServer *mcp.Server
|
mcpServer *mcp.Server
|
||||||
|
|
||||||
@@ -111,11 +114,15 @@ func newHTTPServer(addr string, s *mcp.Server) *http.Server {
|
|||||||
&mcp.StreamableHTTPOptions{
|
&mcp.StreamableHTTPOptions{
|
||||||
Logger: log.Slog(),
|
Logger: log.Slog(),
|
||||||
MaxRequestBodyBytes: maxRequestBodyBytes,
|
MaxRequestBodyBytes: maxRequestBodyBytes,
|
||||||
Stateless: false, // PR 2 switches this on
|
Stateless: false, // SessionTimeout requires stateful sessions.
|
||||||
SessionTimeout: sessionTimeout,
|
SessionTimeout: sessionTimeout,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
return &http.Server{Addr: addr, Handler: mux}
|
return &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: httpReadHeaderTimeout,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() error {
|
func Run() error {
|
||||||
@@ -157,6 +164,8 @@ func Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newMCPServer(version string) *mcp.Server {
|
func newMCPServer(version string) *mcp.Server {
|
||||||
|
// SDK keepalives send MCP ping requests and disconnect clients without a
|
||||||
|
// server-to-client channel, so KeepAlive stays disabled.
|
||||||
s := mcp.NewServer(
|
s := mcp.NewServer(
|
||||||
&mcp.Implementation{
|
&mcp.Implementation{
|
||||||
Name: "Gitea MCP Server",
|
Name: "Gitea MCP Server",
|
||||||
|
|||||||
@@ -2,6 +2,22 @@ package operation
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestNewHTTPServerConfig(t *testing.T) {
|
||||||
|
server := newHTTPServer(":12345", newMCPServer("test"))
|
||||||
|
if server.Addr != ":12345" {
|
||||||
|
t.Errorf("Addr = %q, want %q", server.Addr, ":12345")
|
||||||
|
}
|
||||||
|
if server.Handler == nil {
|
||||||
|
t.Error("Handler is nil")
|
||||||
|
}
|
||||||
|
if server.ReadHeaderTimeout != httpReadHeaderTimeout {
|
||||||
|
t.Errorf("ReadHeaderTimeout = %v, want %v", server.ReadHeaderTimeout, httpReadHeaderTimeout)
|
||||||
|
}
|
||||||
|
if server.WriteTimeout != 0 {
|
||||||
|
t.Errorf("WriteTimeout = %v, want zero for SSE", server.WriteTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseAuthToken(t *testing.T) {
|
func TestParseAuthToken(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import (
|
|||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testServerVersion = "test-version"
|
// Pin negotiated versions so SDK upgrades require compatibility review.
|
||||||
|
const (
|
||||||
|
testServerVersion = "test-version"
|
||||||
|
expectedProtocolVersion = "2026-07-28"
|
||||||
|
expectedStatefulHTTPProtocolVersion = "2025-11-25"
|
||||||
|
)
|
||||||
|
|
||||||
func exposeAllTools(t *testing.T) {
|
func exposeAllTools(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -51,6 +56,21 @@ func registeredToolCount() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stdioCommandEnvironment removes variables that override subprocess flags.
|
||||||
|
func stdioCommandEnvironment() []string {
|
||||||
|
environment := os.Environ()
|
||||||
|
filtered := make([]string, 0, len(environment))
|
||||||
|
for _, entry := range environment {
|
||||||
|
name, _, _ := strings.Cut(entry, "=")
|
||||||
|
switch name {
|
||||||
|
case "GITEA_READONLY", "GITEA_SCOPES", "GITEA_TOOLS", "MCP_MODE":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, entry)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
func textContent(t *testing.T, result *mcp.CallToolResult) string {
|
func textContent(t *testing.T, result *mcp.CallToolResult) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if len(result.Content) != 1 {
|
if len(result.Content) != 1 {
|
||||||
@@ -104,8 +124,8 @@ func TestOfficialSDKInMemory(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Connect() error = %v", err)
|
t.Fatalf("Connect() error = %v", err)
|
||||||
}
|
}
|
||||||
if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" {
|
if got := session.InitializeResult().ProtocolVersion; got != expectedProtocolVersion {
|
||||||
t.Errorf("protocol version = %q, want %q", got, "2026-07-28")
|
t.Errorf("protocol version = %q, want %q", got, expectedProtocolVersion)
|
||||||
}
|
}
|
||||||
listAndCallVersion(ctx, t, session, testServerVersion)
|
listAndCallVersion(ctx, t, session, testServerVersion)
|
||||||
if err := session.Close(); err != nil {
|
if err := session.Close(); err != nil {
|
||||||
@@ -142,8 +162,9 @@ func TestStreamableHTTPStateful(t *testing.T) {
|
|||||||
t.Fatalf("Connect() error = %v", err)
|
t.Fatalf("Connect() error = %v", err)
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
if got := session.InitializeResult().ProtocolVersion; got != "2025-11-25" {
|
// Stateful Streamable HTTP cannot negotiate the sessionless 2026 protocol.
|
||||||
t.Errorf("protocol version = %q, want %q", got, "2025-11-25")
|
if got := session.InitializeResult().ProtocolVersion; got != expectedStatefulHTTPProtocolVersion {
|
||||||
|
t.Errorf("protocol version = %q, want %q", got, expectedStatefulHTTPProtocolVersion)
|
||||||
}
|
}
|
||||||
listAndCallVersion(ctx, t, session, testServerVersion)
|
listAndCallVersion(ctx, t, session, testServerVersion)
|
||||||
|
|
||||||
@@ -347,19 +368,27 @@ func TestStdioCommandTransport(t *testing.T) {
|
|||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping subprocess build in short mode")
|
t.Skip("skipping subprocess build in short mode")
|
||||||
}
|
}
|
||||||
exposeAllTools(t) // the subprocess runs with default flags, so match them here
|
for name, value := range map[string]string{
|
||||||
|
"GITEA_READONLY": "true",
|
||||||
|
"GITEA_SCOPES": "user",
|
||||||
|
"GITEA_TOOLS": "get_me",
|
||||||
|
"MCP_MODE": "http",
|
||||||
|
} {
|
||||||
|
t.Setenv(name, value)
|
||||||
|
}
|
||||||
|
exposeAllTools(t)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
binary := filepath.Join(t.TempDir(), "gitea-mcp")
|
binary := filepath.Join(t.TempDir(), "gitea-mcp")
|
||||||
build := exec.CommandContext(ctx, "go", "build", "-o", binary, "..")
|
build := exec.CommandContext(ctx, "go", "build", "-o", binary, "..")
|
||||||
build.Env = os.Environ()
|
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("build stdio test binary: %v\n%s", err, output)
|
t.Fatalf("build stdio test binary: %v\n%s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-stdio-test", Version: "1"}, nil)
|
client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-stdio-test", Version: "1"}, nil)
|
||||||
command := exec.CommandContext(ctx, binary, "--transport", "stdio")
|
command := exec.CommandContext(ctx, binary, "--transport", "stdio")
|
||||||
|
command.Env = stdioCommandEnvironment()
|
||||||
session, err := client.Connect(ctx, &mcp.CommandTransport{
|
session, err := client.Connect(ctx, &mcp.CommandTransport{
|
||||||
Command: command,
|
Command: command,
|
||||||
TerminateDuration: 2 * time.Second,
|
TerminateDuration: 2 * time.Second,
|
||||||
@@ -368,8 +397,8 @@ func TestStdioCommandTransport(t *testing.T) {
|
|||||||
t.Fatalf("Connect() error = %v", err)
|
t.Fatalf("Connect() error = %v", err)
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" {
|
if got := session.InitializeResult().ProtocolVersion; got != expectedProtocolVersion {
|
||||||
t.Errorf("protocol version = %q, want %q", got, "2026-07-28")
|
t.Errorf("protocol version = %q, want %q", got, expectedProtocolVersion)
|
||||||
}
|
}
|
||||||
listAndCallVersion(ctx, t, session, "Gitea MCP Server version:")
|
listAndCallVersion(ctx, t, session, "Gitea MCP Server version:")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ func (s ServerTool) MCPHandler() mcp.ToolHandler {
|
|||||||
if errors.As(err, &protocolErr) {
|
if errors.As(err, &protocolErr) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Preserve mcp-go behavior; tool-result errors are a separate change.
|
||||||
return nil, internalError(err)
|
return nil, internalError(err)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user