Skip to content

Commit af8bec5

Browse files
sync
2 parents cddc723 + f47e2bc commit af8bec5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5121
-433
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.aider*
22
.env
3-
.idea
3+
.idea
4+
.opencode
5+
.claude

README.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ MCP Go handles all the complex protocol details and server management, so you ca
9191
- [Tools](#tools)
9292
- [Prompts](#prompts)
9393
- [Examples](#examples)
94+
- [Extras](#extras)
95+
- [Session Management](#session-management)
96+
- [Request Hooks](#request-hooks)
97+
- [Tool Handler Middleware](#tool-handler-middleware)
9498
- [Contributing](#contributing)
9599
- [Prerequisites](#prerequisites)
96100
- [Installation](#installation-1)
@@ -516,6 +520,214 @@ For examples, see the `examples/` directory.
516520

517521
## Extras
518522

523+
### Session Management
524+
525+
MCP-Go provides a robust session management system that allows you to:
526+
- Maintain separate state for each connected client
527+
- Register and track client sessions
528+
- Send notifications to specific clients
529+
- Provide per-session tool customization
530+
531+
<details>
532+
<summary>Show Session Management Examples</summary>
533+
534+
#### Basic Session Handling
535+
536+
```go
537+
// Create a server with session capabilities
538+
s := server.NewMCPServer(
539+
"Session Demo",
540+
"1.0.0",
541+
server.WithToolCapabilities(true),
542+
)
543+
544+
// Implement your own ClientSession
545+
type MySession struct {
546+
id string
547+
notifChannel chan mcp.JSONRPCNotification
548+
isInitialized bool
549+
// Add custom fields for your application
550+
}
551+
552+
// Implement the ClientSession interface
553+
func (s *MySession) SessionID() string {
554+
return s.id
555+
}
556+
557+
func (s *MySession) NotificationChannel() chan<- mcp.JSONRPCNotification {
558+
return s.notifChannel
559+
}
560+
561+
func (s *MySession) Initialize() {
562+
s.isInitialized = true
563+
}
564+
565+
func (s *MySession) Initialized() bool {
566+
return s.isInitialized
567+
}
568+
569+
// Register a session
570+
session := &MySession{
571+
id: "user-123",
572+
notifChannel: make(chan mcp.JSONRPCNotification, 10),
573+
}
574+
if err := s.RegisterSession(context.Background(), session); err != nil {
575+
log.Printf("Failed to register session: %v", err)
576+
}
577+
578+
// Send notification to a specific client
579+
err := s.SendNotificationToSpecificClient(
580+
session.SessionID(),
581+
"notification/update",
582+
map[string]any{"message": "New data available!"},
583+
)
584+
if err != nil {
585+
log.Printf("Failed to send notification: %v", err)
586+
}
587+
588+
// Unregister session when done
589+
s.UnregisterSession(context.Background(), session.SessionID())
590+
```
591+
592+
#### Per-Session Tools
593+
594+
For more advanced use cases, you can implement the `SessionWithTools` interface to support per-session tool customization:
595+
596+
```go
597+
// Implement SessionWithTools interface for per-session tools
598+
type MyAdvancedSession struct {
599+
MySession // Embed the basic session
600+
sessionTools map[string]server.ServerTool
601+
}
602+
603+
// Implement additional methods for SessionWithTools
604+
func (s *MyAdvancedSession) GetSessionTools() map[string]server.ServerTool {
605+
return s.sessionTools
606+
}
607+
608+
func (s *MyAdvancedSession) SetSessionTools(tools map[string]server.ServerTool) {
609+
s.sessionTools = tools
610+
}
611+
612+
// Create and register a session with tools support
613+
advSession := &MyAdvancedSession{
614+
MySession: MySession{
615+
id: "user-456",
616+
notifChannel: make(chan mcp.JSONRPCNotification, 10),
617+
},
618+
sessionTools: make(map[string]server.ServerTool),
619+
}
620+
if err := s.RegisterSession(context.Background(), advSession); err != nil {
621+
log.Printf("Failed to register session: %v", err)
622+
}
623+
624+
// Add session-specific tools
625+
userSpecificTool := mcp.NewTool(
626+
"user_data",
627+
mcp.WithDescription("Access user-specific data"),
628+
)
629+
// You can use AddSessionTool (similar to AddTool)
630+
err := s.AddSessionTool(
631+
advSession.SessionID(),
632+
userSpecificTool,
633+
func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
634+
// This handler is only available to this specific session
635+
return mcp.NewToolResultText("User-specific data for " + advSession.SessionID()), nil
636+
},
637+
)
638+
if err != nil {
639+
log.Printf("Failed to add session tool: %v", err)
640+
}
641+
642+
// Or use AddSessionTools directly with ServerTool
643+
/*
644+
err := s.AddSessionTools(
645+
advSession.SessionID(),
646+
server.ServerTool{
647+
Tool: userSpecificTool,
648+
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
649+
// This handler is only available to this specific session
650+
return mcp.NewToolResultText("User-specific data for " + advSession.SessionID()), nil
651+
},
652+
},
653+
)
654+
if err != nil {
655+
log.Printf("Failed to add session tool: %v", err)
656+
}
657+
*/
658+
659+
// Delete session-specific tools when no longer needed
660+
err = s.DeleteSessionTools(advSession.SessionID(), "user_data")
661+
if err != nil {
662+
log.Printf("Failed to delete session tool: %v", err)
663+
}
664+
```
665+
666+
#### Tool Filtering
667+
668+
You can also apply filters to control which tools are available to certain sessions:
669+
670+
```go
671+
// Add a tool filter that only shows tools with certain prefixes
672+
s := server.NewMCPServer(
673+
"Tool Filtering Demo",
674+
"1.0.0",
675+
server.WithToolCapabilities(true),
676+
server.WithToolFilter(func(ctx context.Context, tools []mcp.Tool) []mcp.Tool {
677+
// Get session from context
678+
session := server.ClientSessionFromContext(ctx)
679+
if session == nil {
680+
return tools // Return all tools if no session
681+
}
682+
683+
// Example: filter tools based on session ID prefix
684+
if strings.HasPrefix(session.SessionID(), "admin-") {
685+
// Admin users get all tools
686+
return tools
687+
} else {
688+
// Regular users only get tools with "public-" prefix
689+
var filteredTools []mcp.Tool
690+
for _, tool := range tools {
691+
if strings.HasPrefix(tool.Name, "public-") {
692+
filteredTools = append(filteredTools, tool)
693+
}
694+
}
695+
return filteredTools
696+
}
697+
}),
698+
)
699+
```
700+
701+
#### Working with Context
702+
703+
The session context is automatically passed to tool and resource handlers:
704+
705+
```go
706+
s.AddTool(mcp.NewTool("session_aware"), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
707+
// Get the current session from context
708+
session := server.ClientSessionFromContext(ctx)
709+
if session == nil {
710+
return mcp.NewToolResultError("No active session"), nil
711+
}
712+
713+
return mcp.NewToolResultText("Hello, session " + session.SessionID()), nil
714+
})
715+
716+
// When using handlers in HTTP/SSE servers, you need to pass the context with the session
717+
httpHandler := func(w http.ResponseWriter, r *http.Request) {
718+
// Get session from somewhere (like a cookie or header)
719+
session := getSessionFromRequest(r)
720+
721+
// Add session to context
722+
ctx := s.WithContext(r.Context(), session)
723+
724+
// Use this context when handling requests
725+
// ...
726+
}
727+
```
728+
729+
</details>
730+
519731
### Request Hooks
520732

521733
Hook into the request lifecycle by creating a `Hooks` object with your

client/client.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@ import (
1616
type Client struct {
1717
transport transport.Interface
1818

19-
initialized bool
20-
notifications []func(mcp.JSONRPCNotification)
21-
notifyMu sync.RWMutex
22-
requestID atomic.Int64
23-
capabilities mcp.ServerCapabilities
19+
initialized bool
20+
notifications []func(mcp.JSONRPCNotification)
21+
notifyMu sync.RWMutex
22+
requestID atomic.Int64
23+
clientCapabilities mcp.ClientCapabilities
24+
serverCapabilities mcp.ServerCapabilities
25+
}
26+
27+
type ClientOption func(*Client)
28+
29+
// WithClientCapabilities sets the client capabilities for the client.
30+
func WithClientCapabilities(capabilities mcp.ClientCapabilities) ClientOption {
31+
return func(c *Client) {
32+
c.clientCapabilities = capabilities
33+
}
2434
}
2535

2636
// NewClient creates a new MCP client with the given transport.
@@ -31,10 +41,16 @@ type Client struct {
3141
// if err != nil {
3242
// log.Fatalf("Failed to create client: %v", err)
3343
// }
34-
func NewClient(transport transport.Interface) *Client {
35-
return &Client{
44+
func NewClient(transport transport.Interface, options ...ClientOption) *Client {
45+
client := &Client{
3646
transport: transport,
3747
}
48+
49+
for _, opt := range options {
50+
opt(client)
51+
}
52+
53+
return client
3854
}
3955

4056
// Start initiates the connection to the server.
@@ -132,8 +148,8 @@ func (c *Client) Initialize(
132148
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
133149
}
134150

135-
// Store capabilities
136-
c.capabilities = result.Capabilities
151+
// Store serverCapabilities
152+
c.serverCapabilities = result.Capabilities
137153

138154
// Send initialized notification
139155
notification := mcp.JSONRPCNotification{
@@ -406,3 +422,13 @@ func listByPage[T any](
406422
func (c *Client) GetTransport() transport.Interface {
407423
return c.transport
408424
}
425+
426+
// GetServerCapabilities returns the server capabilities.
427+
func (c *Client) GetServerCapabilities() mcp.ServerCapabilities {
428+
return c.serverCapabilities
429+
}
430+
431+
// GetClientCapabilities returns the client capabilities.
432+
func (c *Client) GetClientCapabilities() mcp.ClientCapabilities {
433+
return c.clientCapabilities
434+
}

client/http.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mark3labs/mcp-go/client/transport"
7+
)
8+
9+
// NewStreamableHttpClient is a convenience method that creates a new streamable-http-based MCP client
10+
// with the given base URL. Returns an error if the URL is invalid.
11+
func NewStreamableHttpClient(baseURL string, options ...transport.StreamableHTTPCOption) (*Client, error) {
12+
trans, err := transport.NewStreamableHTTP(baseURL, options...)
13+
if err != nil {
14+
return nil, fmt.Errorf("failed to create SSE transport: %w", err)
15+
}
16+
return NewClient(trans), nil
17+
}

client/inprocess.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package client
2+
3+
import (
4+
"github.com/mark3labs/mcp-go/client/transport"
5+
"github.com/mark3labs/mcp-go/server"
6+
)
7+
8+
// NewInProcessClient connect directly to a mcp server object in the same process
9+
func NewInProcessClient(server *server.MCPServer) (*Client, error) {
10+
inProcessTransport := transport.NewInProcessTransport(server)
11+
return NewClient(inProcessTransport), nil
12+
}

0 commit comments

Comments
 (0)