Instruction file imported from sbroenne/mcp-server-powerpoint (
.github/instructions/mcp-server-guide.instructions.md). Copyright stays with the author.
MCP Server Development Guide
All tool methods in this project are synchronous — they return
string, notTask<string>. There is no async/await anywhere in theTools/classes; COM access viaIPresentationBatch.Executeis already synchronous from the caller's point of view (the async plumbing lives insidePresentationBatch, not in the tool methods).
LLM-Facing Content Rules
NO EMOJIS in LLM-consumed content — never use emoji characters in:
- Tool
[Description(...)]attributes and XML/// <summary>comments — the MCP SDK extracts these into the tool schema an LLM reads directly. skills/shared/*.mdand any future MCP prompt content.
Use plain text markers: "IMPORTANT:", "WARNING:", "NOTE:", "CRITICAL:".
DO keep emojis in user-facing content: README.md, this repo's own governance docs, and other human-read documentation — humans appreciate visual aids; LLM tool schemas do not need them and they cost tokens for no benefit.
Two Kinds of Tools: Hand-Written vs. Generated
Most of the MCP tool surface is generated, not hand-written. Before editing anything under
Tools/, know which kind you're touching:
- Hand-written (
PresentationTools.csonly): the singlepresentationMCP tool. It is action-dispatch like Excel's file tool, but stays hand-written because create/open/list/close need custom session-registry behavior and optionalsessionId. - Generated (everything else —
slide,shape,textframe,table,notes,layout,master,animation,image,media,chart,smartart,export,pagesetup,accessibility,customshow): one action-dispatch tool per[ServiceCategory]Core domain, emitted byPowerPointMcp.Generators.Mcpfrom the Core interface's[ServiceCategory]/[McpTool]attributes and XML doc comments. Never hand-write a new tool class for one of these domains — add the operation to the Core interface (with XML docs) and the generator picks it up. Seearchitecture-patterns.instructions.md's Command Pattern section for the Core-side attribute shape.
The rest of this guide applies to the hand-written presentation tool only.
Implementation Pattern: Single Hand-Written Dispatch Tool
[McpServerToolType]
public static class PresentationTools
{
private static readonly PresentationCommands Commands = new();
[McpServerTool(Name = "presentation")]
[Description("Presentation lifecycle, template, and document-property operations.")]
public static string Presentation(
PresentationToolAction action,
string? filePath = null,
string? sessionId = null,
PresentationSessionRegistry? registry = null)
=> PowerPointToolsBase.ExecuteToolAction("presentation", action.ToActionString(), () =>
{
return action switch
{
PresentationToolAction.Create => HandleCreate(filePath, false, registry!),
PresentationToolAction.Open => HandleOpen(filePath, registry!),
_ => PowerPointToolsBase.ValidationError($"Unknown action: {action}")
};
});
}
PresentationTools.cs is a single hand-written dispatch tool, not one method-per-verb. Keep new
presentation-lifecycle/template/property work inside that switch. A new Shape/Chart/etc.
operation still goes into Core, not here.
Error Handling (MANDATORY)
MCP tools must return JSON with isError: true for business errors, NOT throw exceptions.
This follows the MCP spec's two error mechanisms:
- Protocol errors — malformed requests, unknown tools → the MCP SDK handles these; tool code rarely needs to throw for this.
- Tool execution errors (business logic failures — unknown session, bad index, missing file)
→ return a JSON payload with
isError: trueviaPowerPointToolsBase.ValidationError(...), do NOT throw.
// CORRECT — expected bad input: return a validation error payload
if (!registry.TryGet(sessionId, out var batch))
{
return PowerPointToolsBase.ValidationError($"Unknown sessionId: {sessionId}");
}
// CORRECT — Core Success=false result: serialize as-is (already has errorMessage + isError)
return SerializeResult(Commands.Delete(batch, slideIndex));
// WRONG — throwing for an expected, caller-correctable condition
if (!registry.TryGet(sessionId, out var batch))
{
throw new InvalidOperationException($"Unknown sessionId: {sessionId}");
}
Unexpected exceptions (COM exceptions, null refs, etc.) are allowed to propagate out of the
tool method body — PowerPointToolsBase.ExecuteToolAction wraps every tool call and catches them
at that single boundary, logging the HResult to stderr and serializing a structured error via
SerializeToolError. Do not add a second try-catch inside an individual tool method — let
ExecuteToolAction be the only catch-all.
Session Injection Pattern (Hand-Written Tools)
PresentationSessionRegistry registry is a plain parameter, not a tool-facing argument — the
MCP SDK (1.3.0+) resolves it from the DI container and correctly excludes it from the generated
JSON schema (verified via tools/list). Every hand-written tool that needs to look up a session
takes it as the last parameter, named exactly registry. Generated action-dispatch tools instead
take a DI-injected PowerPointMcpService service parameter, which the generator wires
automatically — you never write this by hand.
Result Serialization Pattern
Each domain tool class has a private SerializeResult({Domain}OperationResult result) helper
that projects the Core result DTO into the MCP JSON payload:
private static string SerializeResult(ShapeOperationResult result)
=> PowerPointToolsBase.Serialize(new
{
success = result.Success,
errorMessage = result.ErrorMessage,
shapeIndex = result.ShapeIndex,
shapeCount = result.ShapeCount,
isError = result.Success ? (bool?)null : true
});
PowerPointToolsBase.JsonOptions already applies camelCase naming, omits null properties
(DefaultIgnoreCondition.WhenWritingNull), and serializes enums as strings — don't duplicate that
configuration per tool class.
Adding a New Tool
For a generated domain (Slide, Shape, TextFrame, Table, Notes, Layout, Master, Animation, Image, Media, Chart, Export, CustomShow) — the common case:
- Add the Core command +
{Domain}OperationResultfields first (Core-first, tested with real COM pertesting-strategy.instructions.md), with an XML doc<summary>— the generator uses it as the operation's description. - Nothing else to write by hand —
PowerPointMcp.Generators.Mcppicks up the new interface method automatically and adds it as a newactionvalue on that domain's action-dispatch tool (e.g.shape(action: "add-oval", ...)) the next time the project builds. - Verify the new operation appears correctly in
tools/list's schema (protocol test intests/PowerPointMcp.McpServer.Tests) and thatPowerPointMcp.Generators.Cliemitted the matchingpptcli {category} {action}command. - Update
skills/shared/*.md(and its copy underskills/powerpoint-mcp/references/) if the new operation changes recommended workflows.
For a hand-written tool (PresentationTools.cs only) — rare, session-lifecycle/template work:
- Add the Core command first, same as above.
- Add the new enum value + switch arm to
PresentationTools.cs, following the pattern above. - Verify the
presentationtool appears correctly intools/listwith the expected action and no leakedregistryparameter. - Update
skills/shared/*.mdas above.