A few days ago I submitted my first PR to a major open source project: the Model Context Protocol TypeScript SDK. The fix itself was small—ensuring empty object schemas include a required field—but the journey to get there taught me more about debugging than any tutorial ever has.

Here's the story of how I went from a cryptic error message to a merged pull request.

The Problem

I was building a tool server using the MCP SDK when I hit a wall. My tools worked fine in development, but the moment I tried to use them with OpenAI's strict mode, everything broke:

Schema validation failed
The schema has structural issues:
root: Schema must have the following keys: required

The frustrating part? My schema looked completely valid:

server.registerTool(
  "ping",
  {
    description: "Simple health check",
    inputSchema: z.object({}).strict(),
  },
  async () => {
    return { status: "ok" };
  }
);

A ping command with no input parameters. Nothing fancy. But OpenAI refused to accept it.

How I Found It

My first instinct was to blame my own code. I spent an embarrassing amount of time triple-checking my schema definition, reading OpenAI docs, and trying random variations of the same thing.

Eventually, I did what I should have done from the start: I printed out the actual JSON schema being generated.

{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

And that's when I noticed the missing piece. OpenAI strict mode requires the required field to be present on every object schema—even when it's an empty array. The JSON Schema spec says it's optional, but OpenAI says otherwise.

I searched the MCP SDK's GitHub issues and found #1659—someone else had already reported this exact problem. The issue had a few comments, people sharing workarounds like adding dummy parameters just to force required to appear:

// The workaround nobody wanted
inputSchema: z.object({
  _noop: z.string().optional()
}).strict()

Ugly, but it worked. At this point I had two options: use the workaround and move on, or actually fix the problem.

I chose the latter.

The Diagnosis

The MCP SDK uses Zod for schema validation and converts Zod schemas to JSON Schema for wire transmission. The conversion happens in a function called zodToJsonSchema(). I cloned the repo and started digging.

The first thing I learned: schema conversion is more complex than I expected. Zod's z.object() creates a ZodObject type, which gets transformed into a JSON Schema object. The conversion logic handles all the basic stuff—properties, types, descriptions—but it wasn't explicitly setting required when there were no required properties.

Here's the mental model I built as I traced through the code:

  1. User defines Zod schemaz.object({ name: z.string() })
  2. SDK converts to JSON SchemazodToJsonSchema() function
  3. JSON Schema sent over wire → Client receives { type: "object", properties: {...}, required: ["name"] }
  4. OpenAI validates schema → Strict mode enforces additional constraints

The bug was in step 2. When an object had required properties, they got added to the required array. When it had no required properties, the required field was simply omitted. Valid JSON Schema, but invalid for OpenAI.

I found the relevant code in the schema utilities:

if (requiredProperties.length > 0) {
  schema.required = requiredProperties;
}

There it was. The conditional that only added required when there were required properties.

The Fix

The fix seemed simple at first: always set required, even when empty. But as I started writing the code, I realized there was more to it.

Object schemas can appear in many places:

  • Top-level tool input schemas
  • Nested properties ({ user: { name: string } })
  • Array items ({ users: [{ name: string }] })
  • additionalProperties when it's a schema
  • allOf, anyOf, oneOf compositions
  • $defs for reusable schema definitions

I needed to walk the entire schema tree and ensure every object type got the treatment. Missing even one nested object would mean the bug could still surface in complex schemas.

I wrote a recursive function:

function ensureRequiredField(schema: JsonSchema): JsonSchema {
  if (schema.type === 'object') {
    if (!('required' in schema)) {
      schema.required = [];
    }
    
    if (schema.properties) {
      for (const prop of Object.values(schema.properties)) {
        ensureRequiredField(prop);
      }
    }
  }
  
  if (schema.items) {
    ensureRequiredField(schema.items);
  }
  
  for (const key of ['allOf', 'anyOf', 'oneOf']) {
    if (schema[key]) {
      schema[key].forEach(ensureRequiredField);
    }
  }
  
  if (schema.$defs) {
    for (const def of Object.values(schema.$defs)) {
      ensureRequiredField(def);
    }
  }
  
  return schema;
}

Then I wrote tests. Lots of tests:

  • Empty object schemas get required: []
  • Objects with optional-only properties get required: []
  • Deeply nested objects all get required
  • Array items that are objects get required
  • Compositions (anyOf, etc.) get handled correctly

Running the test suite and seeing all green checkmarks was deeply satisfying.

Submitting the PR

I formatted my commit message following the project's conventions, wrote up a description explaining the problem and solution, and linked to issue #1659. Then I hit "Create Pull Request" and waited.

The automated CI checks passed. A maintainer reviewed the code. There was one round of feedback—a suggestion to handle additionalProperties when it's a schema object, not just a boolean. I updated the code, pushed again.

And then the PR got approved.

Seeing those green checkmarks and the "merged" label was one of the most satisfying moments I've had as a developer. My code was now part of a project used by thousands of developers. The next person who tries to create a parameterless tool won't hit the same wall I did.

What I Learned

Read the actual output. I wasted time assuming my code was wrong before actually looking at the generated schema. Print statements are underrated.

Check the issue tracker. Someone else had already documented this problem. If I'd searched earlier, I could have started from a clearer understanding of the bug.

Small fixes can have wide impact. This was maybe 50 lines of code, but it helps every developer building MCP tools for OpenAI. The fix-to-impact ratio was huge.

Tests are your proof. When I submitted the PR, the test cases demonstrated exactly what the fix accomplished. They made the review process smoother because the maintainers could see the expected behavior without running the code themselves.

Open source is approachable. Before this, I thought contributing to major projects required some special skill or deep familiarity with the codebase. It doesn't. You find a bug, understand it, fix it, and submit. The maintainers are people too—they want their software to work.

The PR is merged now. If you've been adding dummy parameters to your empty Zod schemas, the next SDK release will let you delete them.


This was my first contribution to the MCP TypeScript SDK. I'm looking for my next one. If you've run into weird edge cases, let me know—maybe we can fix them together.

React to this post: