Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/test_python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Python SDK Test

on:
push:
branches: [ main ]
paths:
- 'sam-mcp-python/**'
pull_request:
branches: [ main ]
paths:
- 'sam-mcp-python/**'

jobs:
test_python:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Run Python unit tests
run: make test-python

5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.dll
*.so
*.dylib
*.pyc

# Test binary, built with `go test -c`
*.test
Expand All @@ -33,4 +34,6 @@ go.work.sum
#
bin/
tests/e2e/logs/
tests/integration/scratch/
tests/integration/scratch/
__pycache__/
.venv/
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ clean:
test:
CGO_ENABLED=1 go test -v -race -count 1 ./...

.PHONY: test-python test-python-e2e
test-python:
python3 -m venv sam-mcp-python/.venv
./sam-mcp-python/.venv/bin/pip install -e ./sam-mcp-python[test]
./sam-mcp-python/.venv/bin/pytest sam-mcp-python/tests/unit

test-python-e2e: build docker-build
bats --verbose-run tests/e2e/python_sdk_test.bats

e2e-test:
bats --verbose-run tests/e2e/

Expand Down
19 changes: 5 additions & 14 deletions cmd/mcp-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
Expand All @@ -31,14 +29,14 @@ import (
)

func main() {
socketPath := flag.String("socket", "", "Path to Unix domain socket")
serverURL := flag.String("url", "", "MCP server URL (e.g. http://localhost:8080/)")
toolName := flag.String("tool", "get_mesh_info", "Tool to call")
toolArgs := flag.String("args", "{}", "JSON arguments for the tool")
timoutArgs := flag.Int("timeout", 10, "Timeout in seconds")
flag.Parse()

if *socketPath == "" {
log.Fatal("Must specify -socket")
if *serverURL == "" {
log.Fatal("Must specify -url")
}

var ctx context.Context
Expand All @@ -58,21 +56,14 @@ func main() {
cancel()
}()

// Override default HTTP client transport to use Unix socket
http.DefaultClient.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", *socketPath)
},
}

// Create MCP client
client := mcp.NewClient(&mcp.Implementation{
Name: "mcp-test-client",
Version: "0.1.0",
}, nil)

// Connect to server using the URL (host is ignored by custom dialer)
session, err := client.Connect(ctx, &mcp.StreamableClientTransport{Endpoint: "http://localhost/mcp"}, nil)
// Connect to server using the URL
session, err := client.Connect(ctx, &mcp.SSEClientTransport{Endpoint: *serverURL}, nil)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
Expand Down
23 changes: 9 additions & 14 deletions cmd/sam-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -59,7 +58,7 @@ var (
clientSecretFlag string
tokenURLFlag string
hubPublicKeyFlag string
mcpSocketFlag string
mcpAddrFlag string
meshFlag string
discoveryIntervalFlag string
enableRelayFlag bool
Expand Down Expand Up @@ -219,7 +218,7 @@ func main() {
node.Host.SetStreamHandler(api.AuthProtocolID, node.HandleAuthHandshake)

// Start MCP Server
startMCPServer(node, mcpSocketFlag, dataDir)
startMCPServer(node, mcpAddrFlag)

fmt.Printf("SAM Node Online.\nPeerID: %s\nListening on: %v\n", node.Host.ID(), node.Host.Addrs())

Expand Down Expand Up @@ -306,7 +305,7 @@ func main() {
runCmd.Flags().StringVar(&clientIDFlag, "client-id", os.Getenv("SAM_OIDC_ID"), "OIDC Client ID for M2M")
runCmd.Flags().StringVar(&clientSecretFlag, "client-secret", os.Getenv("SAM_OIDC_SECRET"), "OIDC Client Secret for M2M")
runCmd.Flags().StringVar(&hubPublicKeyFlag, "hub-public-key", "", "Hub Public Key (32-byte Hex)")
runCmd.Flags().StringVar(&mcpSocketFlag, "mcp-socket", "", "Path to Unix domain socket for local MCP server (default: <datadir>/mcp.sock)")
runCmd.Flags().StringVar(&mcpAddrFlag, "mcp-addr", "127.0.0.1:8080", "Local TCP address for the MCP HTTP/SSE server")
runCmd.Flags().StringVar(&meshFlag, "mesh", DefaultMeshName, "Mesh federation name")
runCmd.Flags().StringVar(&discoveryIntervalFlag, "discovery-interval", DefaultDiscoveryInterval, "Polling interval for DHT discovery")
runCmd.Flags().BoolVar(&enableRelayFlag, "enable-relay", false, "Allow this node to serve as a relay for others")
Expand Down Expand Up @@ -357,20 +356,16 @@ func getOrGenerateKey(s *Store) crypto.PrivKey {
return priv
}

func startMCPServer(node *SamNode, socketPath string, dataDir string) {
func startMCPServer(node *SamNode, mcpAddr string) {
mcpHandler := NewMCPHandler(node)
go func() {
if socketPath == "" {
socketPath = filepath.Join(dataDir, "mcp.sock")
if mcpAddr == "" {
mcpAddr = "127.0.0.1:8080"
}

if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
logger.Errorf("Failed to remove old socket %s: %v", socketPath, err)
}

listener, err := net.Listen("unix", socketPath)
listener, err := net.Listen("tcp", mcpAddr)
if err != nil {
logger.Errorf("Failed to listen on Unix socket %s: %v", socketPath, err)
logger.Errorf("Failed to listen on TCP address %s: %v", mcpAddr, err)
return
}
defer func() {
Expand All @@ -379,7 +374,7 @@ func startMCPServer(node *SamNode, socketPath string, dataDir string) {
}
}()

fmt.Printf("Starting MCP server on Unix socket %s\n", socketPath)
fmt.Printf("Starting MCP server on TCP address %s\n", listener.Addr().String())
if err := http.Serve(listener, mcpHandler); err != nil {
logger.Errorf("MCP server error: %v", err)
}
Expand Down
34 changes: 22 additions & 12 deletions cmd/sam-node/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ func handleSendMessage(ctx context.Context, req *mcp.CallToolRequest, params Sen
// NewMCPHandler creates a new HTTP handler for the MCP server using the official SDK.
func NewMCPHandler(node *SamNode) http.Handler {
// Create an MCP server.
server := mcp.NewServer(&mcp.Implementation{
mcpServer := mcp.NewServer(&mcp.Implementation{
Name: "sam-node-mcp",
Version: "0.1.0",
}, nil)

// Add the send_message tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "send_message",
Description: "Send a message to another agent in the mesh",
}, handleSendMessage)

// Add the mesh_pubsub_broadcast tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "mesh_pubsub_broadcast",
Description: "Publish an event payload to a custom GossipSub topic",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct {
Expand Down Expand Up @@ -92,7 +92,7 @@ func NewMCPHandler(node *SamNode) http.Handler {
})

// Add the poll_messages tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "poll_messages",
Description: "Poll for incoming messages on custom GossipSub topics",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct {
Expand All @@ -112,7 +112,7 @@ func NewMCPHandler(node *SamNode) http.Handler {
})

// Add the subscribe_topic tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "subscribe_topic",
Description: "Subscribe to a custom GossipSub topic",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct {
Expand All @@ -129,7 +129,7 @@ func NewMCPHandler(node *SamNode) http.Handler {
})

// Add the get_mesh_info tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "get_mesh_info",
Description: "Get information about the mesh network",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct{}) (*mcp.CallToolResult, any, error) {
Expand Down Expand Up @@ -170,7 +170,7 @@ func NewMCPHandler(node *SamNode) http.Handler {
})

// Add the call_remote_tool tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "call_remote_tool",
Description: "Call an MCP tool on a remote agent",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct {
Expand Down Expand Up @@ -198,7 +198,7 @@ func NewMCPHandler(node *SamNode) http.Handler {
})

// Add the connect_peer tool.
mcp.AddTool(server, &mcp.Tool{
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "connect_peer",
Description: "Connect to a peer in the mesh",
}, func(ctx context.Context, req *mcp.CallToolRequest, params struct {
Expand All @@ -222,12 +222,22 @@ func NewMCPHandler(node *SamNode) http.Handler {
}, nil, nil
})

// Create the streamable HTTP handler.
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
return server
// Create the SSE handler using the SDK
sseHandler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server {
return mcpServer
}, nil)

return handler
mux := http.NewServeMux()
mux.Handle("/mcp/events", sseHandler)
mux.Handle("/mcp/message", sseHandler)

// Wrap in logging middleware to debug incoming requests
wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Debugf("MCP Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
mux.ServeHTTP(w, r)
})

return wrappedHandler
}

// CallMCPTool opens a stream to a remote peer, performs the handshake, and calls a tool.
Expand Down
Loading
Loading