[BUG] claude.ai connector ingestion silently drops any tool with inputSchema >16,384 bytes; refresh path applies no limit

Open 💬 0 comments Opened Jul 13, 2026 by mkoker-netcarrier

Summary

When a remote MCP connector is added in Claude Desktop (custom connector, streamable HTTP), the connect-time tool enumeration silently drops any tool whose serialized inputSchema exceeds 16,384 bytes. Measured by bisection with a controlled server (reproducer below):

  • A tool with a 16,384-byte schema loads; 16,385 bytes does not (inclusive boundary, exactly 16 KiB).
  • The limit applies to the schema alone: a tool whose total entry was 18,100 bytes wrapping a 15,000-byte schema loads fine.
  • It is bytes, not tokens: padding is a single repeated character (tokenizes to almost nothing) and still triggers the drop.
  • It is per-tool, not total: with tools deliberately named so the largest sorts first, the large first tool is dropped while smaller later tools load; surviving sets totaling 50KB+ load without issue.

The two defects

  1. The drop is completely silent. No warning in the connector panel, no log, no error anywhere. Our production 4-tool CRUD server showed only its one small tool for a week and was serially misdiagnosed as a server bug, an auth/scope problem, and an Anthropic API change before we bisected the cause.
  1. The refresh path does not enforce the limit. Clicking refresh on the same connector loads tools with 22KB+ schemas. Fresh add and refresh disagreeing makes the behavior look nondeterministic: connect shows 1 tool, refresh shows 4, next fresh add shows 1 again.

Downstream impact: the model confabulates around the silent failure

Because nothing signals that tools were dropped, Claude invents explanations. Two independent sessions in our org, uncoached:

  • One session explained the missing get tool as "a quirk of the tool search/indexing in this session."
  • Another invented "a known recurring quirk" plus "an established workaround: calling the MCP server directly via the Anthropic API inside a widget artifact" — not a real capability; the widget produced nothing, and the lookup only worked after a manual refresh surfaced the real tool.

The silent drop doesn't just hide capability — it manufactures false confidence downstream.

Reproduction (~10 minutes)

Minimal Go server serving tools whose schemas are padded to exact byte sizes; tools are named after their sizes so the connector panel reads itself.

<details>
<summary>go.mod + main.go (complete reproducer)</summary>

module capbisect

go 1.25.0

require github.com/modelcontextprotocol/go-sdk v1.4.1
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"

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

var ladder = []struct {
	name         string
	schemaTarget int // exact serialized size of inputSchema, bytes
}{
	{"a_control_400", 400},
	{"b_s4096", 4096},
	{"c_s8192", 8192},
	{"d_s12288", 12288},
	{"e_s16300", 16300},
	{"f_s16384", 16384}, // the boundary: loads
	{"g_s16385", 16385}, // one byte over: silently dropped
	{"h_s17000", 17000},
	{"i_s20000", 20000},
	{"j_s40000", 40000},
}

func makeTool(name string, schemaTarget int) *mcp.Tool {
	tool := &mcp.Tool{
		Name:        name,
		Description: fmt.Sprintf("probe %s: inputSchema padded to exactly %d bytes.", name, schemaTarget),
		InputSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"note": {Type: "string", Description: "pad:"},
			},
		},
	}
	for i := 0; i < 4; i++ {
		data, err := json.Marshal(tool.InputSchema)
		if err != nil {
			log.Fatal(err)
		}
		diff := schemaTarget - len(data)
		if diff <= 0 {
			break
		}
		schema := tool.InputSchema.(*jsonschema.Schema)
		schema.Properties["note"].Description += strings.Repeat("x", diff)
	}
	return tool
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "18081"
	}
	handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
		srv := mcp.NewServer(&mcp.Implementation{Name: "cap-bisect", Version: "1.0.0"}, nil)
		for _, l := range ladder {
			tool := makeTool(l.name, l.schemaTarget)
			name := l.name
			mcp.AddTool(srv, tool, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
				Note string `json:"note,omitempty"`
			}) (*mcp.CallToolResult, any, error) {
				return &mcp.CallToolResult{Content: []mcp.Content{
					&mcp.TextContent{Text: fmt.Sprintf(`{"tool":%q,"ok":true}`, name)},
				}}, nil, nil
			})
		}
		return srv
	}, &mcp.StreamableHTTPOptions{Stateless: true})

	for _, l := range ladder {
		tool := makeTool(l.name, l.schemaTarget)
		data, _ := json.Marshal(tool.InputSchema)
		log.Printf("%-15s schema=%6d bytes", l.name, len(data))
	}
	log.Printf("cap-bisect listening on :%s/mcp", port)
	log.Fatal(http.ListenAndServe(":"+port, handler))
}

</details>

Steps:

  1. go build && ./capbisect — startup log prints each tool's exact schema byte size (ground truth).
  2. Expose via any tunnel, e.g. cloudflared tunnel --url http://<LAN-IP>:18081 (LAN IP, not localhost — the SDK's DNS-rebinding protection 403s loopback servers receiving a public Host header). Verify tools/list through the tunnel returns all 10 tools — the server side is provably correct.
  3. Claude Desktop → Settings → Connectors → Add custom connector (must be a fresh add; refresh bypasses the cap) → open the connector's Tool permissions panel.
  4. Observed: panel shows exactly 6 tools (a_control_400f_s16384); g_s16385 and everything larger is absent. f_s16384 and g_s16385 differ by one byte.
  5. Click refresh: all 10 appear.

Environment

  • Claude Desktop, Windows, current auto-updated build (July 2026)
  • Custom connector, streamable HTTP, no auth
  • Also reproduced through an org-managed connector against our production MCP server (only the one tool with a 3.9KB schema loaded; tools at 25–104KB dropped, across every server version we've ever shipped)

Related public reports (same silent-cap pattern, different layers)

  • anthropics/claude-code#26406 — Desktop 1.1.3189 (Windows): entire tools/list dropped above ~2,500 bytes total on the local stdio path; distinct bug (whole-list vs per-tool, IPC vs cloud ingestion), same silence. Closed as duplicate, no documented fix.
  • bgheneti/Amazing-Marvin-MCP#11 — Desktop shows 5 of 28 tools; server verified correct via MCP Inspector; log shows [52653 chars truncated]; unresolved.
  • anthropics/claude-code#31302 — complex tool schemas silently truncated, stripped parameters silently dropped from outgoing calls; closed as duplicate.
  • modelcontextprotocol discussion #537 — Desktop silently caps servers at 100 tools.

The pattern across all of these: silent size/count caps discovered only by users binary-searching in the dark. This report adds the first exact constant and a self-contained reproducer.

Requests

  1. Surface dropped tools in the connector UI — name, reason, measured size vs limit.
  2. Document the limit so MCP server authors can budget for it (JSON schemas are routinely 5–10× larger than authors estimate; three separate engineers here "pared down" schemas that were still over the invisible line).
  3. Make connect and refresh enforce the same rule, whichever rule is chosen.

View original on GitHub ↗