Skip to content
Open
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
87 changes: 87 additions & 0 deletions examples/server/custom-method/latinext/latin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

// Package latinext is an example MCP extension that adds a "latin/translate"
// custom JSON-RPC method. It demonstrates the extension-author pattern: types,
// the CustomMethod variable, and the init() registration are all defined here
// so that importers get everything wired up automatically.
package latinext

import (
"context"
"fmt"
"strings"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

// TranslateParams are the parameters for the latin/translate method.
type TranslateParams struct {
mcp.ParamsBase
Text string `json:"text"`
}

// TranslateResult is the result of the latin/translate method.
type TranslateResult struct {
mcp.ResultBase
Latin string `json:"latin"`
}

// Method captures the method name and types once. Extension consumers call
// Translate() rather than this directly.
var Method = mcp.NewCustomMethod[*TranslateParams, *TranslateResult]("latin/translate")

func init() {
mcp.RegisterExtension(mcp.Extension{
Server: func(s *mcp.Server) error {
return Method.RegisterServerReceiving(s, DefaultHandler)
},
Client: func(c *mcp.Client) error {
return Method.RegisterClientSending(c)
},
})
}

// Translate calls the latin/translate method on the server via cs.
// This is the one-liner that extension consumers use — no generics, no method
// name strings.
func Translate(ctx context.Context, cs *mcp.ClientSession, text string) (*TranslateResult, error) {
return Method.Call(ctx, cs, &TranslateParams{Text: text})
}

// DefaultHandler is the reference server-side implementation. It can be
// overridden per-server by calling Method.RegisterServer(server, myHandler).
func DefaultHandler(_ context.Context, _ *mcp.ServerSession, params *TranslateParams) (*TranslateResult, error) {
key := strings.ToLower(strings.TrimSpace(params.Text))
latin, ok := translations[key]
if !ok {
latin = fmt.Sprintf("[unknown: %q]", params.Text)
}
return &TranslateResult{Latin: latin}, nil
}

var translations = map[string]string{
"hello": "salve",
"goodbye": "vale",
"thank you": "gratias tibi ago",
"how are you": "quid agis",
"good morning": "bonum mane",
"good night": "bonam noctem",
"friend": "amicus",
"water": "aqua",
"love": "amor",
"war": "bellum",
"peace": "pax",
"truth": "veritas",
"light": "lux",
"time": "tempus",
"life": "vita",
"death": "mors",
"star": "stella",
"earth": "terra",
"sea": "mare",
"the die is cast": "alea iacta est",
"i came i saw i conquered": "veni vidi vici",
"seize the day": "carpe diem",
}
80 changes: 12 additions & 68 deletions examples/server/custom-method/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,31 @@
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

// The custom-method example demonstrates registering and calling a custom
// JSON-RPC method that is not part of the standard MCP spec.
// The custom-method example demonstrates the extension-author / extension-consumer
// split for custom JSON-RPC methods.
//
// The server registers a "latin/translate" method that translates simple
// English phrases into Latin. A client connects over an in-memory transport,
// calls the custom method, and prints the result.
// The latinext sub-package is the "extension author": it defines the types,
// registers the method via init(), and exposes a domain-specific Translate()
// helper. This file is the "extension consumer": importing latinext is all
// that's needed to wire up both sides.
package main

import (
"context"
"fmt"
"log"
"strings"

"github.com/modelcontextprotocol/go-sdk/examples/server/custom-method/latinext"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

type TranslateParams struct {
mcp.ParamsBase
Text string `json:"text"`
}

type TranslateResult struct {
mcp.ResultBase
Latin string `json:"latin"`
}

var translations = map[string]string{
"hello": "salve",
"goodbye": "vale",
"thank you": "gratias tibi ago",
"how are you": "quid agis",
"good morning": "bonum mane",
"good night": "bonam noctem",
"friend": "amicus",
"water": "aqua",
"love": "amor",
"war": "bellum",
"peace": "pax",
"truth": "veritas",
"light": "lux",
"time": "tempus",
"life": "vita",
"death": "mors",
"star": "stella",
"earth": "terra",
"sea": "mare",
"the die is cast": "alea iacta est",
"i came i saw i conquered": "veni vidi vici",
"seize the day": "carpe diem",
}

func main() {
ctx := context.Background()

// NewServer and NewClient automatically apply all extensions registered via
// init() — including latinext's handler and sending registration.
server := mcp.NewServer(&mcp.Implementation{Name: "latin-server", Version: "v1.0.0"}, nil)

if err := mcp.AddReceivingCustomMethod(server, "latin/translate",
func(ctx context.Context, ss *mcp.ServerSession, params *TranslateParams) (*TranslateResult, error) {
key := strings.ToLower(strings.TrimSpace(params.Text))
latin, ok := translations[key]
if !ok {
latin = fmt.Sprintf("[unknown: %q — try: %s]", params.Text, knownPhrases())
}
return &TranslateResult{Latin: latin}, nil
}); err != nil {
log.Fatal(err)
}
client := mcp.NewClient(&mcp.Implementation{Name: "latin-client", Version: "v1.0.0"}, nil)

ct, st := mcp.NewInMemoryTransports()

Expand All @@ -79,32 +36,19 @@ func main() {
}
defer ss.Close()

client := mcp.NewClient(&mcp.Implementation{Name: "latin-client", Version: "v1.0.0"}, nil)
if err := mcp.AddSendingCustomMethod[*TranslateParams, *TranslateResult](client, "latin/translate"); err != nil {
log.Fatal(err)
}

cs, err := client.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()

// Call the custom method — no generics, no method-name strings.
phrases := []string{"Hello", "Seize the day", "Peace", "Truth", "I came I saw I conquered"}
for _, phrase := range phrases {
result, err := mcp.CallCustomMethod[*TranslateParams, *TranslateResult](
ctx, cs, "latin/translate", &TranslateParams{Text: phrase})
result, err := latinext.Translate(ctx, cs, phrase)
if err != nil {
log.Fatalf("translate %q: %v", phrase, err)
}
fmt.Printf("%-35s → %s\n", phrase, result.Latin)
}
}

func knownPhrases() string {
phrases := make([]string, 0, len(translations))
for k := range translations {
phrases = append(phrases, fmt.Sprintf("%q", k))
}
return strings.Join(phrases, ", ")
}
51 changes: 50 additions & 1 deletion mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type Client struct {
// serverMethodInfos) plus any custom methods registered via
// [AddSendingCustomMethod].
sendMethods map[string]methodInfo
// receiveMethods is the merged map of methods this client may receive from
// a server: it always contains the standard client methods (from
// clientMethodInfos) plus any custom methods registered via
// [AddClientReceivingCustomMethod].
receiveMethods map[string]methodInfo
}

// NewClient creates a new [Client].
Expand Down Expand Up @@ -68,17 +73,23 @@ func NewClient(impl *Implementation, options *ClientOptions) *Client {
sendMethods := make(map[string]methodInfo, len(serverMethodInfos))
maps.Copy(sendMethods, serverMethodInfos)

receiveMethods := make(map[string]methodInfo, len(clientMethodInfos))
maps.Copy(receiveMethods, clientMethodInfos)

c := &Client{
impl: impl,
opts: opts,
roots: newFeatureSet(func(r *Root) string { return r.URI }),
sendingMethodHandler_: defaultSendingMethodHandler,
receivingMethodHandler_: defaultReceivingMethodHandler[*ClientSession],
sendMethods: sendMethods,
receiveMethods: receiveMethods,
}
if opts.MultiRoundTrip == nil || !opts.MultiRoundTrip.Disabled {
c.AddSendingMiddleware(clientMultiRoundTripMiddleware())
}
applyExtensionsToClient(c)
runExtensions(opts.Extensions, func(e Extension) func(*Client) error { return e.Client }, c)
return c
}

Expand Down Expand Up @@ -204,6 +215,10 @@ type ClientOptions struct {
// reset" guidance, letting a transient miss pass without tearing down an
// otherwise live session. Has no effect unless KeepAlive is non-zero.
KeepAliveFailureThreshold int
// Extensions are applied to the client during [NewClient], after any
// globally registered extensions (see [RegisterExtension]). Per-client
// extensions override global ones for the same method names.
Extensions []Extension
}

// toolContextKeyType is the context key type for passing tool definitions
Expand Down Expand Up @@ -1170,7 +1185,9 @@ func (cs *ClientSession) sendingMethodInfos() map[string]methodInfo {
}

func (cs *ClientSession) receivingMethodInfos() map[string]methodInfo {
return clientMethodInfos
cs.client.mu.Lock()
defer cs.client.mu.Unlock()
return cs.client.receiveMethods
}

func (cs *ClientSession) handle(ctx context.Context, req *jsonrpc.Request) (any, error) {
Expand Down Expand Up @@ -1674,3 +1691,35 @@ func CallCustomMethod[P paramsPtr[PT], R Result, PT any](
Params: params,
})
}

// AddClientReceivingCustomMethod registers a handler for a custom
// (non-standard) JSON-RPC method that the client may receive from a server.
//
// When a server sends a request with the given method name, the params will be
// unmarshaled into P, the handler will be called, and the returned R will be
// marshaled as the JSON-RPC result.
//
// P and R must implement [Params] and [Result] respectively, which is most
// easily done by embedding [ParamsBase] and [ResultBase].
//
// AddClientReceivingCustomMethod returns an error if method is the name of a
// standard MCP method. Registering the same custom method twice replaces the
// previous handler.
func AddClientReceivingCustomMethod[P paramsPtr[T], R Result, T any](
c *Client,
method string,
handler func(ctx context.Context, cs *ClientSession, params P) (R, error),
) error {
if _, ok := clientMethodInfos[method]; ok {
return fmt.Errorf("mcp: AddClientReceivingCustomMethod: %q shadows a standard MCP method", method)
}

typed := typedClientMethodHandler[P, R](func(ctx context.Context, req *ClientRequest[P]) (R, error) {
return handler(ctx, req.Session, req.Params)
})

c.mu.Lock()
defer c.mu.Unlock()
c.receiveMethods[method] = newClientMethodInfo(typed, missingParamsOK)
return nil
}
Loading
Loading