Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

404 lines
15 KiB
Go

package mcpserver_test
import (
"bufio"
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"time"
implementationhost "github.com/tutti-os/tutti/packages/connector/runtime/implementationhost"
connectormcpserver "github.com/tutti-os/tutti/packages/connector/runtime/mcpserver"
)
const protocolVersion = "2026-07-28"
type scopedRouterStub struct {
toolsScope connectormcpserver.RequestScope
callScope connectormcpserver.RequestScope
contextOK bool
}
func (router *scopedRouterStub) Tools(ctx context.Context, scope connectormcpserver.RequestScope) ([]implementationhost.MCPTool, error) {
router.toolsScope = scope
contextScope, ok := connectormcpserver.RequestScopeFromContext(ctx)
router.contextOK = ok && contextScope == scope
return []implementationhost.MCPTool{{
Name: "documents_search", InputSchema: map[string]any{"type": "object", "properties": map[string]any{}},
}}, nil
}
func (router *scopedRouterStub) CallValidated(
ctx context.Context,
scope connectormcpserver.RequestScope,
_ string,
_ map[string]any,
validate func(implementationhost.MCPTool) error,
) (json.RawMessage, error) {
router.callScope = scope
contextScope, ok := connectormcpserver.RequestScopeFromContext(ctx)
router.contextOK = router.contextOK && ok && contextScope == scope
tool := implementationhost.MCPTool{Name: "documents_search", InputSchema: map[string]any{"type": "object", "properties": map[string]any{}}}
if validate != nil {
if err := validate(tool); err != nil {
return nil, err
}
}
return json.RawMessage(`{"resultType":"complete","content":[]}`), nil
}
func (*scopedRouterStub) Subscribe(connectormcpserver.RequestScope) (<-chan struct{}, func()) {
updates := make(chan struct{})
return updates, func() { close(updates) }
}
func TestSessionRouterReceivesBearerDerivedScope(t *testing.T) {
router := &scopedRouterStub{}
server, err := connectormcpserver.Start(connectormcpserver.Config{SessionRouter: router})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
binding, err := server.Bind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 7,
})
if err != nil {
t.Fatal(err)
}
listing := postModernRPC(t, binding.URL, binding.Headers["Authorization"], 1, "tools/list", map[string]any{})
listing.Body.Close()
wantScope := connectormcpserver.RequestScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 7,
}
if listing.StatusCode != http.StatusOK || router.toolsScope != wantScope || !router.contextOK {
t.Fatalf("tools scope=%#v contextOK=%v status=%d", router.toolsScope, router.contextOK, listing.StatusCode)
}
call := postModernRPC(t, binding.URL, binding.Headers["Authorization"], 2, "tools/call", map[string]any{
"name": "documents_search", "arguments": map[string]any{},
})
call.Body.Close()
if call.StatusCode != http.StatusOK || router.callScope != router.toolsScope || !router.contextOK {
t.Fatalf("call scope=%#v contextOK=%v status=%d", router.callScope, router.contextOK, call.StatusCode)
}
}
func TestServerServesProviderNativeMCP(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
binding, err := server.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
initialized := postProviderRPC(t, binding.URL, binding.Headers["Authorization"], 1, "initialize", map[string]any{
"protocolVersion": "2025-06-18",
"capabilities": map[string]any{},
"clientInfo": map[string]any{"name": "codex", "version": "1"},
})
defer initialized.Body.Close()
var initializePayload struct {
Result struct {
ProtocolVersion string `json:"protocolVersion"`
ServerInfo struct {
Name string `json:"name"`
} `json:"serverInfo"`
} `json:"result"`
}
if initialized.StatusCode != http.StatusOK || json.NewDecoder(initialized.Body).Decode(&initializePayload) != nil ||
initializePayload.Result.ProtocolVersion != "2025-06-18" || initializePayload.Result.ServerInfo.Name != "connector" {
t.Fatalf("initialize status=%d payload=%#v", initialized.StatusCode, initializePayload)
}
listing := postProviderRPC(t, binding.URL, binding.Headers["Authorization"], 2, "tools/list", map[string]any{})
defer listing.Body.Close()
var listPayload struct {
Result struct {
Tools []implementationhost.MCPTool `json:"tools"`
} `json:"result"`
}
if listing.StatusCode != http.StatusOK || json.NewDecoder(listing.Body).Decode(&listPayload) != nil || listPayload.Result.Tools == nil {
t.Fatalf("tools/list status=%d payload=%#v", listing.StatusCode, listPayload)
}
resources := postProviderRPC(t, binding.URL, binding.Headers["Authorization"], 3, "resources/list", map[string]any{})
defer resources.Body.Close()
var resourcesPayload struct {
Result struct {
Resources []any `json:"resources"`
} `json:"result"`
}
if resources.StatusCode != http.StatusOK || json.NewDecoder(resources.Body).Decode(&resourcesPayload) != nil || resourcesPayload.Result.Resources == nil {
t.Fatalf("resources/list status=%d payload=%#v", resources.StatusCode, resourcesPayload)
}
}
func TestBindRejectsPartialInvocationScopeAndLegacyBindingRemainsCompatible(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
invalid := []connectormcpserver.BindingScope{
{WorkspaceID: "workspace-1", AgentSessionID: "session-1", InvocationID: "invocation-1"},
{WorkspaceID: "workspace-1", AgentSessionID: "session-1", InvocationGeneration: 1},
}
for _, scope := range invalid {
if _, err := server.Bind(scope); err == nil {
t.Fatalf("partial Invocation scope was accepted: %#v", scope)
}
}
if _, err := server.Binding("workspace-1", "session-1"); err != nil {
t.Fatalf("legacy Session binding failed: %v", err)
}
}
func TestServerIssuesSessionScopedBindingAndServesModernMCP(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_ = server.Close(ctx)
})
binding, err := server.Binding("workspace-1", "session-1")
if err != nil || binding.Name != "connector" || binding.Type != "http" || !strings.HasPrefix(binding.URL, "http://127.0.0.1:") {
t.Fatalf("binding = %#v, err = %v", binding, err)
}
if response := postModernRPC(t, binding.URL, "", 1, "server/discover", map[string]any{}); response.StatusCode != http.StatusUnauthorized {
response.Body.Close()
t.Fatalf("unauthorized status = %d", response.StatusCode)
}
discover := postModernRPC(t, binding.URL, binding.Headers["Authorization"], 2, "server/discover", map[string]any{})
defer discover.Body.Close()
var discovered struct {
Result struct {
ResultType string `json:"resultType"`
SupportedVersions []string `json:"supportedVersions"`
} `json:"result"`
}
if discover.StatusCode != http.StatusOK || json.NewDecoder(discover.Body).Decode(&discovered) != nil ||
discovered.Result.ResultType != "complete" || len(discovered.Result.SupportedVersions) != 1 || discovered.Result.SupportedVersions[0] != protocolVersion {
t.Fatalf("server/discover status=%d payload=%#v", discover.StatusCode, discovered)
}
listing := postModernRPC(t, binding.URL, binding.Headers["Authorization"], 3, "tools/list", map[string]any{})
defer listing.Body.Close()
var payload struct {
Result struct {
ResultType string `json:"resultType"`
Tools []implementationhost.MCPTool `json:"tools"`
} `json:"result"`
}
if listing.StatusCode != http.StatusOK || json.NewDecoder(listing.Body).Decode(&payload) != nil ||
payload.Result.ResultType != "complete" || len(payload.Result.Tools) != 0 {
t.Fatalf("tools/list status=%d payload=%#v", listing.StatusCode, payload)
}
server.Revoke("workspace-1", "session-1")
if response := postModernRPC(t, binding.URL, binding.Headers["Authorization"], 4, "tools/list", map[string]any{}); response.StatusCode != http.StatusUnauthorized {
response.Body.Close()
t.Fatalf("revoked status = %d", response.StatusCode)
}
}
func TestBindingReplacementAndRevokeAllInvalidateTokens(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
first, err := server.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
second, err := server.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
response := postModernRPC(t, first.URL, first.Headers["Authorization"], 1, "tools/list", map[string]any{})
response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
t.Fatalf("replaced token status=%d", response.StatusCode)
}
response = postModernRPC(t, second.URL, second.Headers["Authorization"], 2, "tools/list", map[string]any{})
response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("replacement token status=%d", response.StatusCode)
}
server.RevokeAll()
response = postModernRPC(t, second.URL, second.Headers["Authorization"], 3, "tools/list", map[string]any{})
response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
t.Fatalf("revoke-all token status=%d", response.StatusCode)
}
}
func TestServerRejectsLegacyMethodsInvalidOriginAndHeaderMismatch(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
binding, err := server.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
request, _ := http.NewRequest(http.MethodGet, binding.URL, nil)
request.Header.Set("Authorization", binding.Headers["Authorization"])
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("GET status=%d", response.StatusCode)
}
request = modernRPCRequest(t, binding.URL, binding.Headers["Authorization"], 1, "tools/list", map[string]any{})
request.Header.Set("Origin", "https://attacker.example")
response, err = http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusForbidden {
t.Fatalf("invalid-origin status=%d", response.StatusCode)
}
request = modernRPCRequest(t, binding.URL, binding.Headers["Authorization"], 2, "tools/list", map[string]any{})
request.Header.Set("Mcp-Method", "tools/call")
response, err = http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusBadRequest {
t.Fatalf("header-mismatch status=%d", response.StatusCode)
}
}
func TestSubscriptionIsAcknowledgedAndCompletesOnBindingRevocation(t *testing.T) {
server, err := connectormcpserver.Start(connectormcpserver.Config{Registry: implementationhost.NewMCPRegistry()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close(context.Background()) })
binding, err := server.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
request := modernRPCRequest(t, binding.URL, binding.Headers["Authorization"], 7, "subscriptions/listen", map[string]any{
"notifications": map[string]any{"toolsListChanged": true},
})
requestContext, cancel := context.WithTimeout(request.Context(), 3*time.Second)
defer cancel()
response, err := http.DefaultClient.Do(request.WithContext(requestContext))
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK || response.Header.Get("Content-Type") != "text/event-stream" {
t.Fatalf("subscription response status=%d content-type=%q", response.StatusCode, response.Header.Get("Content-Type"))
}
reader := bufio.NewReader(response.Body)
acknowledged := readSSEPayload(t, reader)
if acknowledged["method"] != "notifications/subscriptions/acknowledged" {
t.Fatalf("subscription acknowledgement = %#v", acknowledged)
}
server.Revoke("workspace-1", "session-1")
completed := readSSEPayload(t, reader)
result, _ := completed["result"].(map[string]any)
if completed["id"] != float64(7) || result["resultType"] != "complete" {
t.Fatalf("subscription completion = %#v", completed)
}
}
func readSSEPayload(t *testing.T, reader *bufio.Reader) map[string]any {
t.Helper()
for {
line, err := reader.ReadString('\n')
if err != nil {
t.Fatalf("read SSE payload: %v", err)
}
if !strings.HasPrefix(line, "data: ") {
continue
}
var payload map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(line, "data: "))), &payload); err != nil {
t.Fatalf("decode SSE payload: %v", err)
}
return payload
}
}
func postModernRPC(t *testing.T, endpoint, authorization string, id int, method string, params map[string]any) *http.Response {
t.Helper()
request := modernRPCRequest(t, endpoint, authorization, id, method, params)
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func postProviderRPC(t *testing.T, endpoint, authorization string, id int, method string, params map[string]any) *http.Response {
t.Helper()
raw, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": id, "method": method, "params": params})
if err != nil {
t.Fatal(err)
}
request, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Authorization", authorization)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json, text/event-stream")
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func modernRPCRequest(t *testing.T, endpoint, authorization string, id int, method string, params map[string]any) *http.Request {
t.Helper()
if params == nil {
params = map[string]any{}
}
params["_meta"] = map[string]any{
"io.modelcontextprotocol/protocolVersion": protocolVersion,
"io.modelcontextprotocol/clientInfo": map[string]any{
"name": "connector-mcp-test", "version": "1",
},
"io.modelcontextprotocol/clientCapabilities": map[string]any{},
}
raw, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": id, "method": method, "params": params})
if err != nil {
t.Fatal(err)
}
request, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json, text/event-stream")
request.Header.Set("MCP-Protocol-Version", protocolVersion)
request.Header.Set("Mcp-Method", method)
if name, _ := params["name"].(string); name != "" {
request.Header.Set("Mcp-Name", name)
}
if authorization != "" {
request.Header.Set("Authorization", authorization)
}
return request
}