fix(mcp): map tool-not-found to CodeNotFound via registry sentinel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,7 @@ func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request) {
|
|||||||
out, err := s.opts.Registry.Dispatch(r.Context(), p.Name, p.Arguments)
|
out, err := s.opts.Registry.Dispatch(r.Context(), p.Name, p.Arguments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
code := -32000
|
code := -32000
|
||||||
if errors.Is(err, ErrToolNotFound) {
|
if errors.Is(err, registry.ErrToolNotFound) {
|
||||||
code = CodeNotFound
|
code = CodeNotFound
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK,
|
writeJSON(w, http.StatusOK,
|
||||||
@@ -125,8 +125,6 @@ func (s *Server) handleGET(w http.ResponseWriter, r *http.Request) {
|
|||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrToolNotFound = errors.New("tool not found")
|
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|||||||
@@ -117,3 +117,31 @@ func TestPostBodyTooLarge(t *testing.T) {
|
|||||||
assert.NotEqual(t, http.StatusOK, rr.Code, "oversized body must not return 200")
|
assert.NotEqual(t, http.StatusOK, rr.Code, "oversized body must not return 200")
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToolsCallToolNotFound(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
// Initialize to get a session ID.
|
||||||
|
init := postJSON(t, srv, map[string]any{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": map[string]any{"protocolVersion": "2025-06-18"},
|
||||||
|
}, "")
|
||||||
|
sid := init.Header().Get("Mcp-Session-Id")
|
||||||
|
|
||||||
|
rr := postJSON(t, srv, map[string]any{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": map[string]any{"name": "nonexistent", "arguments": map[string]any{}},
|
||||||
|
}, sid)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||||
|
rpcErr, ok := resp["error"].(map[string]any)
|
||||||
|
require.True(t, ok, "expected error field in response")
|
||||||
|
code := int(rpcErr["code"].(float64))
|
||||||
|
assert.Equal(t, -32002, code, "expected CodeNotFound (-32002) for missing tool")
|
||||||
|
assert.NotEmpty(t, rpcErr["message"])
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrToolNotFound = errors.New("tool not found")
|
||||||
|
|
||||||
type ToolDescriptor struct {
|
type ToolDescriptor struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@@ -36,7 +39,7 @@ func (r *Registry) Tools() []ToolDescriptor {
|
|||||||
func (r *Registry) Dispatch(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) {
|
func (r *Registry) Dispatch(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) {
|
||||||
t, ok := r.tools[name]
|
t, ok := r.tools[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("tool not found: " + name)
|
return nil, fmt.Errorf("tool %q: %w", name, ErrToolNotFound)
|
||||||
}
|
}
|
||||||
return t.Call(ctx, args)
|
return t.Call(ctx, args)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user