TypedJsonRpc and Schema Generation
Added in: v1.0.0
Auto-Schema: v1.8.0+
Status: Production-ready
Overview
TypedJsonRpc<T> provides type-safe parameter handling with automatic JSON Schema generation from your C# types.
Benefits:
- β Type-safe - Compile-time validation
- β Auto-schema - No manual JSON Schema needed
- β IntelliSense - Full IDE support
- β Nullable - C# nullable reference types respected
- β
Descriptions -
[Description]attributes included - β Enums - Automatic enum value lists
- β Formats - DateTime, Guid, etc. auto-detected
Quick Example
Before (Manual Schema)
[McpTool("greet",
InputSchema = @"{
""type"":""object"",
""properties"":{
""name"":{""type"":""string"",""description"":""User name""}
},
""required"":[""name""]
}")]
public JsonRpcMessage Greet(JsonRpcMessage request)
{
var name = request.GetParams().GetProperty("name").GetString();
return ToolResponse.Success(request.Id, new { message = $"Hello, {name}!" });
}
After (Auto-Schema)
[McpTool("greet")]
public JsonRpcMessage Greet(TypedJsonRpc<GreetParams> request)
{
var args = request.GetParams()!;
return ToolResponse.Success(request.Id,
new { message = $"Hello, {args.Name}!" });
}
public sealed record GreetParams(
[property: Description("User name")] string Name);
Result: Same JSON Schema, less code! β¨
Output Schema Generation (v1.8.0+)
Just like input parameters, you can now strongly type your toolβs output and get automatic outputSchema generation.
Example
[McpTool("add_numbers")]
public TypedJsonRpc<AddResponse> Add(TypedJsonRpc<AddParams> request)
{
var args = request.GetParams()!;
return TypedJsonRpc<AddResponse>.Success(
request.Id,
new AddResponse(args.A + args.B));
}
public sealed record AddResponse(
[property: Description("The sum of the two numbers")]
double Result);
Generated Output Schema:
{
"type": "object",
"properties": {
"result": {
"type": "number",
"description": "The sum of the two numbers"
}
},
"required": ["result"]
}
Benefits:
- β
Structured Content - Automatically populates
structuredContent - β Validation - Clients can validate the response against the schema
- β Documentation - LLMs understand exactly what the tool returns
How It Works
1. Type Mapping
ToolSchemaGenerator automatically maps C# types to JSON Schema types:
| C# Type | JSON Type | Format |
|---|---|---|
string |
"string" |
- |
int, long, short, byte |
"integer" |
- |
float, double, decimal |
"number" |
- |
bool |
"boolean" |
- |
Guid |
"string" |
"uuid" |
DateTime, DateTimeOffset |
"string" |
"date-time" |
enum |
"string" |
- (with enum values) |
T[], List<T> |
"array" |
- |
| Complex types | "object" |
- |
2. Nullable Detection
C# nullable reference types are respected:
public sealed record MyParams(
string Name, // Required (non-nullable)
string? Email); // Optional (nullable)
Generated schema:
{
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["name"]
}
3. Description Attributes
Use [Description] from System.ComponentModel:
using System.ComponentModel;
public sealed record UserParams(
[property: Description("User's full name")]
string Name,
[property: Description("User's email address")]
string? Email);
Generated schema:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User's full name"
},
"email": {
"type": "string",
"description": "User's email address"
}
},
"required": ["name"]
}
4. Enum Support
Enums automatically generate allowed values:
public enum Priority { Low, Medium, High }
public sealed record TaskParams(
[property: Description("Task priority level")]
Priority Priority);
Generated schema:
{
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["Low", "Medium", "High"],
"description": "Task priority level"
}
},
"required": ["priority"]
}
5. JsonPropertyName
Control JSON property names:
using System.Text.Json.Serialization;
public sealed record UserParams(
[property: JsonPropertyName("full_name")]
string FullName);
Generated schema:
{
"type": "object",
"properties": {
"full_name": {"type": "string"}
},
"required": ["full_name"]
}
Default: Property names are automatically converted to camelCase:
FullNameβ"fullName"EmailAddressβ"emailAddress"
Supported Features
β Automatic Features
| Feature | Support | Example |
|---|---|---|
| Type mapping | β Full | string, int, double, etc. |
| Nullable | β Full | string? β optional |
| Description | β Full | [Description("...")] |
| Enum | β Full | enum β "enum": [...] |
| Formats | β Partial | Guid, DateTime |
| Arrays | β Basic | string[], List<T> |
| Nested objects | β Basic | type: "object" |
| Default values | β Not yet | Use InputSchema |
β οΈ Limitations
Auto-schema generation does NOT support:
| Feature | Workaround |
|---|---|
anyOf, oneOf, allOf |
Use manual InputSchema |
if/then/else |
Use manual InputSchema |
| Array item schemas | Use manual InputSchema |
| Min/Max constraints | Use manual InputSchema |
| Pattern (regex) | Use manual InputSchema |
| Custom formats | Use manual InputSchema |
| Recursive types | Use manual InputSchema |
For advanced schemas: Use explicit InputSchema parameter!
Complete Examples
Example 1: Simple Parameters
[McpTool("create_user")]
public JsonRpcMessage CreateUser(TypedJsonRpc<CreateUserParams> request)
{
var args = request.GetParams()!;
// args.Name, args.Email, args.Age are strongly typed
return ToolResponse.Success(request.Id, new { userId = 123 });
}
public sealed record CreateUserParams(
[property: Description("User's full name")]
string Name,
[property: Description("User's email address")]
string Email,
[property: Description("User's age in years")]
int? Age);
Auto-generated schema:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User's full name"
},
"email": {
"type": "string",
"description": "User's email address"
},
"age": {
"type": "integer",
"description": "User's age in years"
}
},
"required": ["name", "email"]
}
Example 2: Enum and DateTime
[McpTool("create_task")]
public JsonRpcMessage CreateTask(TypedJsonRpc<CreateTaskParams> request)
{
var args = request.GetParams()!;
return ToolResponse.Success(request.Id, new { taskId = 456 });
}
public enum TaskStatus { Pending, InProgress, Completed }
public sealed record CreateTaskParams(
[property: Description("Task title")]
string Title,
[property: Description("Task status")]
TaskStatus Status,
[property: Description("Due date (ISO 8601)")]
DateTime? DueDate);
Auto-generated schema:
{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Task title"
},
"status": {
"type": "string",
"enum": ["Pending", "InProgress", "Completed"],
"description": "Task status"
},
"dueDate": {
"type": "string",
"format": "date-time",
"description": "Due date (ISO 8601)"
}
},
"required": ["title", "status"]
}
Example 3: Arrays
[McpTool("send_email")]
public JsonRpcMessage SendEmail(TypedJsonRpc<SendEmailParams> request)
{
var args = request.GetParams()!;
// args.Recipients is string[]
return ToolResponse.Success(request.Id, new { sent = true });
}
public sealed record SendEmailParams(
[property: Description("Email subject")]
string Subject,
[property: Description("Email body")]
string Body,
[property: Description("List of recipient email addresses")]
string[] Recipients);
Auto-generated schema:
{
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "Email subject"
},
"body": {
"type": "string",
"description": "Email body"
},
"recipients": {
"type": "array",
"description": "List of recipient email addresses"
}
},
"required": ["subject", "body", "recipients"]
}
Example 4: Guid and Custom Names
[McpTool("update_resource")]
public JsonRpcMessage UpdateResource(TypedJsonRpc<UpdateResourceParams> request)
{
var args = request.GetParams()!;
return ToolResponse.Success(request.Id, new { updated = true });
}
public sealed record UpdateResourceParams(
[property: JsonPropertyName("resource_id")]
[property: Description("Unique resource identifier")] Guid ResourceId,
[property: JsonPropertyName("resource_name")]
[property: Description("Resource name")] string ResourceName);
Auto-generated schema:
{
"type": "object",
"properties": {
"resource_id": {
"type": "string",
"format": "uuid",
"description": "Unique resource identifier"
},
"resource_name": {
"type": "string",
"description": "Resource name"
}
},
"required": ["resource_id", "resource_name"]
}
When to Use InputSchema
Use explicit InputSchema when you need:
1. Union Types (anyOf, oneOf)
[McpTool("process_input",
InputSchema = @"{
""type"":""object"",
""properties"":{
""value"":{
""oneOf"":[
{""type"":""string""},
{""type"":""number""}
]
}
}
}")]
public JsonRpcMessage ProcessInput(JsonRpcMessage request)
{
var value = request.GetParams().GetProperty("value");
// Manual handling of union type
}
2. Conditional Schemas (if/then/else)
[McpTool("conditional_tool",
InputSchema = @"{
""type"":""object"",
""properties"":{
""type"":{""type"":""string"",""enum"":[""A"",""B""]},
""value"":{""type"":""string""}
},
""if"":{""properties"":{""type"":{""const"":""A""}}},
""then"":{""required"":[""value""]}
}")]
3. Array Item Schemas
[McpTool("process_items",
InputSchema = @"{
""type"":""object"",
""properties"":{
""items"":{
""type"":""array"",
""items"":{
""type"":""object"",
""properties"":{
""id"":{""type"":""integer""},
""name"":{""type"":""string""}
}
}
}
}
}")]
4. Min/Max Constraints
[McpTool("create_user",
InputSchema = @"{
""type"":""object"",
""properties"":{
""age"":{
""type"":""integer"",
""minimum"":18,
""maximum"":100
},
""name"":{
""type"":""string"",
""minLength"":1,
""maxLength"":50
}
}
}")]
5. Pattern Matching
[McpTool("validate_email",
InputSchema = @"{
""type"":""object"",
""properties"":{
""email"":{
""type"":""string"",
""pattern"":""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$""
}
}
}")]
Hybrid Approach
Combine auto-schema with validation:
[McpTool("create_user")]
public JsonRpcMessage CreateUser(TypedJsonRpc<CreateUserParams> request)
{
var args = request.GetParams()!;
// Manual validation for complex rules
if (args.Age.HasValue && (args.Age < 18 || args.Age > 100))
{
throw new ToolInvalidParamsException("Age must be between 18 and 100");
}
if (args.Name.Length > 50)
{
throw new ToolInvalidParamsException("Name too long (max 50 chars)");
}
return ToolResponse.Success(request.Id, new { userId = 123 });
}
public sealed record CreateUserParams(
[property: Description("User's name (max 50 characters)")]
string Name,
[property: Description("User's age (18-100)")]
int? Age);
Benefits:
- β Auto-schema for basic validation
- β Manual validation for complex rules
- β Type-safe parameters
- β Clear error messages
Best Practices
1. Always Use Sealed Records
// β
GOOD - Sealed record
public sealed record MyParams(string Name);
// β BAD - Class (mutable)
public class MyParams
{
public string Name { get; set; }
}
2. Add Descriptions
// β
GOOD - Documented
public sealed record CreateUserParams(
[property: Description("User's full name")] string Name);
// β οΈ OK - But less helpful
public sealed record CreateUserParams(string Name);
3. Use Nullable Appropriately
// β
GOOD - Clear intent
public sealed record SearchParams(
string Query, // Required
int? Limit, // Optional
string? Sort); // Optional
// β BAD - Everything nullable
public sealed record SearchParams(
string? Query, // Should be required!
int? Limit,
string? Sort);
4. JsonPropertyName for API Consistency
// β
GOOD - Explicit naming
public sealed record UserParams(
[property: JsonPropertyName("user_id")] Guid UserId,
[property: JsonPropertyName("user_name")] string UserName);
// β οΈ OK - Auto camelCase
public sealed record UserParams(Guid UserId, string UserName);
// β {"userId": "...", "userName": "..."}
Performance
Auto-schema generation:
- When: Once during tool discovery (startup)
- Cost: ~5-10ms per tool (one-time)
- Memory: Cached in
ToolService - Runtime: Zero overhead (pre-generated)
No performance impact during tool invocations!
Troubleshooting
Schema Not Generated
Problem: Tool uses TypedJsonRpc<T> but no schema appears
Solutions:
- Check if
InputSchemais explicitly set (it takes priority) - Ensure
Tis a public type - Verify properties are public
- Check logs for schema generation errors
Wrong Property Names
Problem: JSON property names donβt match C# names
Solution: Use [JsonPropertyName]:
public sealed record MyParams(
[property: JsonPropertyName("my_field")] string MyField);
Enum Not Working
Problem: Enum values not appearing in schema
Solution: Ensure enum is public and non-nested:
// β
GOOD
public enum Status { Active, Inactive }
// β BAD - Nested enum
public class MyClass
{
private enum Status { ... } // Won't work!
}
See Also
- Tools API - Complete Tools API reference
- Getting Started - Build your first tool
- DateTime Example - Real-world TypedJsonRpc usage