Skip to main content
The Qyl.Agents.Generator package is an incremental source generator that transforms [McpServer] classes into fully functional MCP servers at build time.

How It Works

  1. Extraction — scans for [McpServer] classes and [Tool] methods
  2. Modeling — builds value-equatable models (incremental generator cache-friendly)
  3. Generation — emits a single .g.cs file per server with all dispatch, schema, OTel, and metadata code

Server Declaration

using Qyl.Agents;

/// <summary>A document search server</summary>
[McpServer]
public partial class DocTools
{
    /// <summary>Search documents by query</summary>
    [Tool]
    public string Search(
        [Description("Query text")] string query,
        int limit = 10) => query;
}
The class must be partial — the generator adds the IMcpServer implementation.

McpServer Attribute

PropertyTypeDefaultDescription
Namestring?Class name, kebab-casedServer name in MCP protocol
Descriptionstring?XML doc summaryServer description
Versionstring?Assembly versionSemantic version

Tool Attribute

PropertyTypeDefaultDescription
Namestring?Method name, kebab-casedTool name in MCP protocol
Descriptionstring?XML doc summaryTool description
ReadOnlyToolHintUnsetHint: tool does not modify state
IdempotentToolHintUnsetHint: repeated calls have same effect
DestructiveToolHintUnsetHint: tool may delete or destroy data
OpenWorldToolHintUnsetHint: tool interacts with external systems
Safety hints are emitted in tools/list only when non-Unset. ToolHint has three values: Unset (omitted), True, False.
[Tool(ReadOnly = ToolHint.True, Idempotent = ToolHint.True)]
public string Search([Description("Query")] string query) => query;

Generated Output

For each [McpServer] class, the generator produces a single .g.cs file containing:

Tool Dispatch

A DispatchToolCallAsync method that routes by tool name using a switch expression:
public async Task<string> DispatchToolCallAsync(
    string toolName,
    JsonElement arguments,
    CancellationToken cancellationToken = default)
{
    return toolName switch
    {
        "search" => await ExecuteTool_SearchAsync(arguments, cancellationToken),
        _ => throw new ArgumentException($"Unknown tool: {toolName}")
    };
}

Parameter Deserialization

Primitives use direct JsonElement accessors (AOT-safe). Complex types fall back to JsonSerializer.
TypeAccessorAOT-safe
stringGetString()Yes
intGetInt32()Yes
boolGetBoolean()Yes
DateTimeOffsetGetDateTimeOffset()Yes
GuidGetGuid()Yes
Urinew Uri(GetString())Yes
EnumsEnum.Parse<T>(GetString())Yes
string[], custom typesJsonSerializer.Deserialize<T>()No

JSON Schema

Each tool gets a static s_schema_{MethodName} byte array with the JSON Schema for its input parameters, including types, formats, required fields, descriptions, and enum values.

OpenTelemetry Instrumentation

Every tool call emits:
  • An ActivitySource("Qyl.Agents") span with gen_ai.* semantic conventions
  • A gen_ai.client.operation.duration histogram for latency tracking
  • Error status and error.type tag on exceptions

SKILL.md

A SkillMd static property with the full SKILL.md content including YAML frontmatter, tool descriptions, and parameter documentation.

LLMS.txt

A LlmsTxt static property with a summary of the server’s capabilities, including tools, resources, and prompts sections.

Resources

Expose read-only data via [Resource]:
[McpServer]
public partial class ConfigServer
{
    [Resource("config://app-settings")]
    public Task<string> GetAppSettings(CancellationToken ct) =>
        File.ReadAllTextAsync("appsettings.json", ct);
}

Resource Attribute

PropertyTypeDefaultDescription
Uristring(required)Resource URI for MCP protocol
Namestring?Method nameDisplay name
Descriptionstring?XML doc summaryResource description
MimeTypestring?nullContent MIME type
The generator emits DispatchResourceReadAsync for URI-based dispatch and GetResourceInfos for resources/list. Resource methods must return string, Task<string>, or ValueTask<string>.

Prompts

Expose reusable prompt templates via [Prompt]:
[McpServer]
public partial class MyServer
{
    /// <summary>Summarize a document</summary>
    [Prompt]
    public string Summarize([Description("Text to summarize")] string text) =>
        $"Please summarize the following:\n\n{text}";

    /// <summary>Code review with structured messages</summary>
    [Prompt]
    public PromptResult Review([Description("Code to review")] string code) =>
        new()
        {
            Messages =
            [
                new() { Role = "user", Content = $"Review this code:\n\n{code}" }
            ]
        };
}
  • string return → wrapped as a single user-role message
  • PromptResult return → structured messages passed through directly
The generator emits DispatchPromptAsync and GetPromptInfos for prompts/list and prompts/get. Prompt methods must return string, Task<string>, PromptResult, or Task<PromptResult>.

Supported Parameter Types

CategoryTypes
Primitivesstring, bool, int, long, double, float, decimal, byte, sbyte, short, ushort, uint, ulong
SpecialDateTimeOffset, DateTime, Guid, Uri
EnumsAny enum type (schema includes allowed values)
NullableT? for all above
ComplexArrays, custom objects (requires JsonSerializer)
ControlCancellationToken (passed through, not in schema)

Supported Return Types

TypeSerialization
voidReturns "null"
stringDirect passthrough
bool"true" / "false"
Numeric typesToString(InvariantCulture)
Task / ValueTaskReturns "null"
Task<T> / ValueTask<T>Same rules as sync T
Complex typesJsonSerializer.Serialize<T>()