MCP TypeScript inputSchema Empty? Fix z.object on v1

MCP TypeScript inputSchema Empty? Fix z.object on v1

Aident AI

An empty cyan folded frame sits beside a green folded tray that securely holds amber and coral forms.

MCP TypeScript inputSchema Empty? Fix z.object on v1

If a TypeScript MCP server publishes an empty tool inputSchema, check the SDK major version and the registration API before changing the client. Legacy v1 server.tool() expects a raw Zod shape such as { name: z.string() }. The v2 registerTool() API expects a Standard Schema such as z.object({ name: z.string() }).

Using the v2 form on an affected v1 positional server.tool() call can publish {"type":"object"} with no properties, strip every argument before the handler, or fail tools/list with an internal _def error. Use the schema form that matches the installed SDK, reconnect the client, and verify the actual tools/list response.

This guide is specifically about the TypeScript SDK's raw-shape versus ZodObject boundary. If tools/list already contains the correct fields but a client sends one object as a quoted string, use the separate empty property schema guide.

Recognize the Version-Mismatch Signature

Match all of these conditions:

  • the server uses the TypeScript MCP SDK;

  • the tool is registered with server.tool() or server.registerTool();

  • source code declares one or more input fields;

  • the discovered top-level inputSchema has no properties, or tools/list fails while converting the schema; and

  • the handler receives {} or never receives a valid call.

A current upstream report tested the failure across v1 releases. On older v1 versions, a ZodObject supplied to the wrong legacy overload could either crash tools/list or silently produce an empty object schema. More recent v1 releases improved normalization and errors, but the v1 and v2 contracts still differ.

Do not diagnose this from TypeScript source alone. The contract the model sees is the JSON Schema returned by tools/list.

Check the Installed Package and API

Start with a clean working tree and identify the package actually resolved by the server:

pnpm why @modelcontextprotocol/sdk
pnpm list @modelcontextprotocol/sdk --depth 0

Then inspect the registration call.

The legacy v1 positional form looks like this:

server.tool(
  "greet",
  "Greet one person",
  z.object({ name: z.string() }),
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}` }],
  }),
);

On an affected v1 path, z.object(...) is the wrong schema form for that position. Do not assume it is correct just because it is idiomatic Zod or appears in v2 documentation.

The v2 form uses a configuration object:

server.registerTool(
  "greet",
  {
    description: "Greet one person",
    inputSchema: z.object({ name: z.string() }),
  },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}` }],
  }),
);

That z.object(...) is the native v2 contract. Swapping the forms without checking the installed major version can move the bug rather than fix it.

Fix a v1 server.tool Call With a Raw Shape

For the affected v1 positional API, pass the raw shape directly:

const greetInput = {
  name: z.string().min(1).describe("Person to greet"),
};

server.tool("greet", "Greet one person", greetInput, async ({ name }) => ({
  content: [{ type: "text", text: `Hello, ${name}` }],
}));

Keep one source of truth. Do not retain a raw shape for discovery and a separate z.object(...) for runtime validation. The registration API should derive both discovery metadata and handler validation from the same definition.

Rebuild the server with its owning package command, stop the old process, and reconnect the client. A source edit does not prove that the running process or current client session has rediscovered the tool.

Expected result:

{
  "name": "greet",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "minLength": 1,
        "description": "Person to greet"
      }
    },
    "required": ["name"]
  }
}

If the discovered schema still lacks name, stop. You may be rebuilding a different package, launching an older output directory, or reconnecting to another server command.

Migrate to v2 Without Inverting the Fix

The official v1-to-v2 guide recommends running the codemod first, then reviewing every marker and type error. The codemod replaces legacy registration calls and wraps raw input shapes in z.object(...) for the v2 API.

Run it only on a reviewable branch:

git status --short
pnpm dlx @modelcontextprotocol/codemod@latest v1-to-v2 .
rg -n "@mcp-codemod-error" .
pnpm exec tsc --noEmit

Review the diff before accepting it. The migration changes package imports, registration APIs, handler context, transports, errors, and other contracts beyond schemas. Do not manually wrap every raw shape before running the codemod, because that makes the source harder to classify and can recreate the v1 failure before the package migration lands.

After migration, the intended v2 pattern is:

const GreetInput = z.object({
  name: z.string().min(1).describe("Person to greet"),
});

server.registerTool(
  "greet",
  {
    description: "Greet one person",
    inputSchema: GreetInput,
  },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}` }],
  }),
);

Expected result: the v2 package type-checks, tools/list advertises name, a valid call reaches the handler, and an invalid call fails before the handler runs.

Add a Discovery Contract Test

A handler unit test cannot detect a broken advertised schema if it calls the function directly. Add an in-memory client test that exercises the protocol boundary:

const { tools } = await client.listTools();
const greet = tools.find((tool) => tool.name === "greet");

expect(greet?.inputSchema).toMatchObject({
  type: "object",
  properties: {
    name: {
      type: "string",
    },
  },
  required: ["name"],
});

const result = await client.callTool({
  name: "greet",
  arguments: { name: "Ada" },
});

expect(result.content).toContainEqual({
  type: "text",
  text: "Hello, Ada",
});

Also test a missing name. It should return a validation error and must not enter a side-effecting handler.

This test protects the entire path:

source schema -> tools/list JSON Schema -> tools/call arguments -> handler

Separate Similar Empty-Schema Bugs

An empty schema has more than one possible cause.

Discovered behavior

Likely boundary

Next action

v1 server.tool() plus z.object(...) loses every field

Raw shape versus ZodObject mismatch

Pass the v1 raw shape or migrate the whole registration to v2

v2 registerTool() receives a raw shape

v1 syntax retained during migration

Run and review the official codemod, then use a Standard Schema

Plain fields work, but .refine() or .superRefine() becomes {}

Wrapped Zod schema conversion

Track the separate ZodEffects issue and do not claim the raw-shape fix covers it

Top-level fields are correct, but one property schema is {}

Untyped property contract

Follow the stringified argument guide

tools/list is correct, but the application receives wrong types

Client generation, transport, or application parsing

Capture the raw tools/call frame before changing the schema

The TypeScript SDK has separate reports for discriminated unions and ZodEffects wrappers. Do not flatten a union or remove cross-field validation merely to make an empty schema disappear. Reduce the case to a plain object, identify the failing wrapper, and follow the matching upstream issue.

Common Mistakes

Copying v2 documentation into a v1 server

Current v2 examples correctly use z.object(...). They are not evidence that the same value belongs in every v1 overload.

Patching the client

The client cannot infer fields that the server omitted from discovery. Fix the server contract first, then reconnect.

Parsing {} in the handler

Adding defaults or manually decoding arguments hides the missing discovery contract. The model still cannot see the fields, and invalid input may reach a write.

Keeping two schema definitions

Two definitions drift. Use the registration API's supported schema value as the shared source for discovery and validation.

Calling migration complete after the codemod

The codemod is a mechanical first pass. Type-check, inspect every marker, run protocol tests, and verify the deployed server command.

Verify Upstream State With Aident Loadout

Start with the canonical setup instruction:

Follow https://aident.ai/SETUP.md

Then give your agent this bounded request:

Check Aident Loadout authentication and Vault status. Search the staging capability catalog for the current read-only GitHub issue lookup Action, inspect its schema, and preflight exact lookups for modelcontextprotocol/typescript-sdk issues 2627, 1291, and 2145. If every estimate is valid and free, execute the lookups and return only issue number, state, updated time, title, comment count, labels, and canonical URL. Do not comment, react, edit, close, create, install, change files, or expose credentials.

The measurable result is three current issue records and zero provider writes. Use that check before repeating an old workaround in a release note or support reply.

Set up Aident Loadout and verify the MCP schema issues read-only.

For a smaller tool surface before debugging, use How to Reduce MCP Token Usage in Claude Code and Codex. For another version-sensitive SDK migration, see MCP Python SDK 2 McpError ImportError.

Sources

Refresh this guide when issue 2627 changes state, the recommended v1 release changes its schema normalization, the v2 registration contract changes, or issue 2145 receives a released fix.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

The one tool

for every tool

your agent needs.

Give any AI agent real capabilities in seconds. Connect 1,000+ tools once, skip the setup headache, and let your agents execute.

Try Aident Loadout

Give your Agent real capabilities in minutes. Connect 1,000+ tools, and let your agents execute.

Try Aident Loadout

Give your Agent real capabilities in minutes. Connect 1,000+ tools, and let your agents execute.

Try Aident Loadout

Give your Agent real capabilities in minutes. Connect 1,000+ tools, and let your agents execute.