Skills, Code Execution and the MCP Connector in the Claude API: Using Ready-Made Capabilities and Remote MCP Servers
To produce files such as Excel in the Claude API you send the xlsx skill in container.skills together with the code execution tool and download the file_ids returned from $OUTPUT_DIR with the Files API; a remote MCP server is connected through the mcp_servers and mcp_toolset pair with a beta header. Sandbox limits and pricing, uploading a custom SKILL.md, pause_turn and the container, allowlists, token scope, and the SDK's null and list behavior.
Version note: The code in this post was built with .NET 10, Anthropic C# SDK 13.8.0 and Claude API version 2026-10-21, and its unit tests were run. The tests use the SDK's real client with a fake HttpMessageHandler that never touches the network; the structure of the fake responses comes from the Files API, code execution, Skills and MCP connector pages, and the ids are made up for the tests. Behavior, limits and prices were taken on 15 November 2026 from the platform.claude.com documentation. This post is based on the official documentation.
Guess First
At the start of every month Selin wants a management report for Bookstore, the online bookstore used as the shared example across this site's series: daily revenue, the best-selling books, a chart and the current stock of the best sellers. Today Emre prepares it by hand in an Excel file. The new idea: give the sales data to Claude, let Claude produce the Excel file, and take the stock figures from the remote MCP server we wrote for Bookstore in the tenth post of the AI Agents and MCP series.
What is your guess? Who produces the Excel file, where does it run, and how does it come back to you? Does Bookstore.Api make the call to the MCP server, or does the Claude API? And is there a point in this flow where customer data could leak? In this post we look at three parts of the Claude API that run on its own side: the code execution tool, the Skills API and the MCP connector. Agent patterns and writing MCP servers belong to the AI Agents and MCP series; here we cover using that server from the API.
Code Execution: Claude's Sandbox
The code execution tool is one of the server tools from the fifth post: you put its definition into the request, but Anthropic's container runs the code and the result comes in the same response. According to the documentation the tool gives Claude two sub-tools: bash_code_execution, which runs shell commands, and text_editor_code_execution, which creates and edits files. There are three current versions: code_execution_20250825 covers bash and file operations, code_execution_20260120 adds REPL state and programmatic tool calling, and code_execution_20260521 brings the same runtime with a description that tells the model about the 90-second limit on Python cells. None of them needs a beta header. Every example on the Skills page uses the first version, and so do we.
The sandbox limits are clear in the documentation: Python 3.11, Linux, x86_64, 5 GiB of memory, 5 GiB of disk, 1 CPU. Internet access is fully disabled and packages cannot be installed at run time; libraries such as pandas, openpyxl, matplotlib, python-pptx and pypdf come preinstalled. Containers belong to the workspace and expire 30 days after creation. A container idle for about 5 minutes is checkpointed, and a request with the same id restores it.
Pricing has two parts. Used together with web search or web fetch, code execution has no extra charge. Used on its own, execution time is billed separately from tokens: at least 5 minutes are counted, each organization gets 1,550 free hours per month, and after that it costs 0.05 USD per hour per container. If the request contains files, the time is billed even when the tool is never called, because the files are preloaded into the container.
Skills: Ready-Made and Our Own Capabilities
A skill is a folder of instructions, scripts and resource files. According to the Skills page, the Skills API has not been in beta since 19 August 2026. There are two sources: Anthropic's ready-made skills (pptx, xlsx, docx, pdf; versions in date format or latest) and the custom skills you upload (with a skill_... id, versions skver_... or latest, visible only in your own workspace). Both are used the same way: a skills list in the request's container parameter, with the code execution tool next to it. A request can have up to 20 skills.
The documentation describes the loading order like this: Claude first sees only the name and description of each skill in the system prompt, the skill files are copied into the container under /skills/{skill-name}/, and the full instructions are read only when needed. That is why the description line matters; Claude decides when to use a skill by looking at it.
We wrote a small custom skill for Bookstore's report rules:
---
name: bookstore-report
description: Bookstore's conventions for management reports built from sales data. Use when creating an Excel or PDF report about Bookstore sales, revenue or stock.
---
# Bookstore report conventions
## Workbook layout
- First sheet: `Summary` with the month, total revenue, number of orders and number of books sold.
- Second sheet: `Daily revenue` with one row per day and a line chart of revenue.
- Third sheet: `Top titles` with the ten best-selling titles, sorted by revenue.
- If stock data is available, a fourth sheet: `Stock` with the current stock of the top titles.
## Formatting
- Amounts are in Turkish lira; use the number format `#,##0.00 "TRY"`.
- Dates use `yyyy-mm-dd`.
- Freeze the header row on every sheet.
## Rules
- Use only the data in the uploaded files and the tool results. Never invent numbers.
- Do not add customer names, e-mail addresses or other personal data to any sheet.
- Name the file `sales-report-YYYY-MM.xlsx` and save it to `$OUTPUT_DIR`.
The rules in the documentation: SKILL.md must be at the root of the upload or at the top of a single enclosing folder; name is at most 64 characters of lowercase letters, digits and hyphens and cannot contain anthropic or claude; description is at most 1,024 characters; the whole upload is under 30 MB. The upload code sends the file names together with the folder:
public static class SkillUploader
{
// File names keep the folder ("bookstore-report/SKILL.md"), so SKILL.md sits at the top of one enclosing folder.
public static SkillCreateParams BuildCreateParams(string skillDirectory)
{
string root = Path.GetFullPath(skillDirectory);
string folder = new DirectoryInfo(root).Name;
List<BinaryContent> files = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
.Order(StringComparer.Ordinal)
.Select(path => new BinaryContent
{
Stream = File.OpenRead(path),
FileName = folder + "/" + Path.GetRelativePath(root, path).Replace('\\', '/'),
})
.ToList();
if (!files.Any(f => f.FileName == folder + "/SKILL.md"))
{
throw new InvalidOperationException($"{skillDirectory} has no SKILL.md at its top level.");
}
return new SkillCreateParams { Files = files };
The upload happens once, and the returned skill_... id goes into configuration. The documentation stresses one warning separately: since skills carry instructions and code, use only skills you wrote yourself or that come from Anthropic. An untrusted skill can steer Claude into using tools and code in unexpected ways.
MCP Connector: Let the API Call the Remote Server
The MCP connector lets the Claude API connect to a remote MCP server on your behalf; Bookstore.Api needs no MCP client of its own. The feature is in beta and has two headers: mcp-client-2025-11-20 and mcp-client-2026-09-15, which includes everything the older one does. The documentation recommends the new one; it also lets you record and pin the tool list each server returns, so a server that changes its tools in the middle of a conversation does not change the list Claude sees. The plan was written when only the older header existed; we use the new one.
The setup has two parts, and both are required: connection details in the mcp_servers array (type: url, an address starting with https://, a unique name, and an authorization_token if needed), and an mcp_toolset in the tools array that refers to that server by name. According to the documentation's validation rules, each server must be used by exactly one toolset; sending only mcp_servers and forgetting the toolset is a validation error. The limits matter too: only tool calls from the MCP spec are supported (no prompts or resources), and the server must be exposed to the internet over HTTP; Streamable HTTP and SSE are accepted, local stdio servers cannot be connected. The Claude API cannot reach a localhost server on your development machine.
In the C# SDK the MCP connector is in the beta namespace: client.Beta.Messages and the Anthropic.Models.Beta.Messages types. Code execution and skills also work with the non-beta client.Messages, but we want everything in one request, so we build the report with the beta API.
Bookstore: The Monthly Sales Report
The method that builds the request:
public static MessageCreateParams BuildRequest(
IReadOnlyList<BetaMessageParam> messages, string? containerId, SalesReportOptions settings)
{
bool useInventory = !string.IsNullOrWhiteSpace(settings.InventoryMcpUrl);
List<BetaToolUnion> tools = [new BetaCodeExecutionTool20250825()];
if (useInventory)
{
// mcp_servers and mcp_toolset go together: every server must be used by exactly one toolset.
tools.Add(new BetaMcpToolset
{
McpServerName = InventoryServerName,
// Allowlist: only the read-only stock tool, whatever else the server offers.
DefaultConfig = new BetaMcpToolDefaultConfig { Enabled = false },
Configs = new Dictionary<string, BetaMcpToolConfig> { ["get_stock_levels"] = new() { Enabled = true } },
});
}
List<BetaSkillParams> skills = [new() { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest" }];
if (!string.IsNullOrWhiteSpace(settings.ConventionsSkillId))
{
skills.Add(new() { Type = BetaSkillParamsType.Custom, SkillID = settings.ConventionsSkillId, Version = "latest" });
}
BetaContainerParams container = new() { Skills = skills };
// The SDK serializes a property when it is set: build lists first, assign them once.
MessageCreateParams request = new()
{
Model = settings.Model,
MaxTokens = 8192,
// After the first request the same container keeps the files of this turn; ID = null would send "id": null.
Container = containerId is null ? container : container with { ID = containerId },
Messages = messages,
Tools = tools,
};
if (!useInventory)
{
return request;
}
return request with
{
McpServers =
[
new BetaRequestMcpServerUrlDefinition
{
Name = InventoryServerName,
Url = settings.InventoryMcpUrl!,
AuthorizationToken = settings.InventoryMcpToken,
},
],
Betas = [AnthropicBeta.McpClient2026_09_15],
};
}
There are three decisions. The skill list has a fixed order; according to the Skills page, changing the skill list breaks the prompt cache. The toolset is an allowlist: default_config.enabled = false, and only get_stock_levels is on. Even if the server adds a tool that cancels orders one day, Claude does not see it. The documentation also recommends turning off tools that write in read-only assistants. The third is the token: it comes from configuration, not from the code or appsettings.json:
public sealed class SalesReportOptions
{
public const string SectionName = "SalesReport";
public string Model { get; set; } = "claude-sonnet-5-5";
// skill_... id of the uploaded bookstore-report skill (SkillUploader). Empty: only the Anthropic xlsx skill.
public string? ConventionsSkillId { get; set; }
// Remote MCP server with Bookstore's inventory tools (AI Agents and MCP series). Empty: no MCP connector.
public string? InventoryMcpUrl { get; set; }
// A read-only token for that server, from user secrets or a vault; never from appsettings.json.
public string? InventoryMcpToken { get; set; }
}
The flow itself:
try
{
List<BetaMessageParam> messages =
[
new BetaUserMessageParam
{
Content = new List<BetaContentBlockParam>
{
new BetaContainerUploadBlockParam { FileID = upload.ID },
new BetaTextBlockParam
{
Text = $"""
The uploaded CSV has Bookstore's book sales for {year}-{month:00}.
Build an Excel management report with the xlsx skill: revenue per day, top titles and a chart.
If the inventory tool is available, add a sheet with the current stock of the top titles.
Save the workbook as sales-report-{year}-{month:00}.xlsx in $OUTPUT_DIR and list the directory.
Then reply with a three-sentence summary.
""",
},
},
},
];
string? containerId = null;
List<string> outputFileIds = [];
BetaMessage response;
int requests = 0;
do
{
response = await claude.Beta.Messages.Create(BuildRequest(messages, containerId, options.Value), cancellationToken);
requests++;
containerId = response.Container?.ID ?? containerId;
outputFileIds.AddRange(OutputFileIds(response));
// pause_turn: send the turn back unchanged in the same container so Claude can continue.
messages.Add(new BetaAssistantMessageParam
{
Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
});
}
while (response.StopReason?.Value() == BetaStopReason.PauseTurn && requests < MaxRequests);
List<ReportFile> files = [];
foreach (string fileId in outputFileIds.Distinct())
{
files.Add(await DownloadAndDeleteAsync(fileId, cancellationToken));
}
string summary = string.Concat(response.Content.Select(b => b.TryPickText(out BetaTextBlock? t) ? t.Text : string.Empty));
logger.LogInformation("Sales report {Year}-{Month}: stop={StopReason} requests={Requests} files={Files}",
year, month, response.StopReason?.Raw(), requests, files.Count);
return new SalesReport(summary.Trim(), files, requests);
}
finally
{
// The CSV was only for this run.
await claude.Files.Delete(upload.ID, cancellationToken: CancellationToken.None);
}
The CSV is uploaded with the Files API from the eighth post and enters the message through a container_upload block; according to the documentation that is how you give a data file to code execution. The CSV holds only the order number, date, ISBN, title and price, no customer information. Data you do not send to the sandbox cannot leak.
A long skill run can stop with stop_reason: pause_turn. According to the documentation, if you send the response back as it is, Claude continues its turn; the container id must be sent too, otherwise the request runs in a new container and the files produced so far are lost. The loop does this with at most four requests.
How do the produced files come back? According to the documentation, every bash_code_execution call gets an empty $OUTPUT_DIR folder; when the command finishes, the files at the top level of this folder are captured and returned as file_id values in the result. A file written anywhere else stays in the container. That is why the documentation suggests telling Claude explicitly to copy the file into $OUTPUT_DIR and list the folder; this is the reason for the sentence "save ... in $OUTPUT_DIR and list the directory" in the prompt.
// Files written to $OUTPUT_DIR come back as file ids inside bash_code_execution results.
public static IEnumerable<string> OutputFileIds(BetaMessage response) =>
response.Content
.Select(block => block.TryPickBashCodeExecutionToolResult(out BetaBashCodeExecutionToolResultBlock? result) ? result : null)
.Where(result => result is not null)
.SelectMany(result => result!.Content.TryPickBetaBashCodeExecutionResultBlock(out BetaBashCodeExecutionResultBlock? ok)
? ok.Content.Select(output => output.FileID)
: []);
private async Task<ReportFile> DownloadAndDeleteAsync(string fileId, CancellationToken cancellationToken)
{
FileMetadata metadata = await claude.Files.RetrieveMetadata(fileId, cancellationToken: cancellationToken);
using Anthropic.Core.HttpResponse download = await claude.Files.Download(fileId, cancellationToken: cancellationToken);
await using Stream content = await download.ReadAsStream(cancellationToken);
using MemoryStream buffer = new();
await content.CopyToAsync(buffer, cancellationToken);
// Generated files count toward the organization's storage until deleted.
await claude.Files.Delete(fileId, cancellationToken: cancellationToken);
return new ReportFile(metadata.Filename, buffer.ToArray());
}
}
Files you upload yourself could not be downloaded; files produced by skills and code execution can. After downloading we delete both the CSV and the produced file; both stay in the organization's storage until they are deleted.
Two Surprises in the SDK
While writing the tests we ran into two behaviors of SDK 13.8.0; both silently change the request body.
First, assigning null to a property is not the same as leaving the field out. In the first version we wrote ID = containerId; on the first request containerId was null, so "id": null went into the body. We saw the same thing with the diagnostics field in the seventh post. The documentation does not say how the API treats this, so we set the field only when there is a value.
Second, the SDK serializes a property at the moment it is set. In the first version we added the mcp_toolset to the list after the Tools = tools assignment; the test showed that the toolset was not in the body. A small experiment confirms it:
List<MessageParam> messages = [new UserMessageParam { Content = "first" }];
MessageCreateParams request = new() { Model = "claude-haiku-5-5", MaxTokens = 10, Messages = messages };
messages.Add(new UserMessageParam { Content = "added later" });
Console.WriteLine(JsonSerializer.Serialize(request.RawBodyData));
{"model":"claude-haiku-5-5","max_tokens":10,"messages":[{"role":"user","content":"first"}]}
The rule is simple: build lists completely first, then assign them. If you need a change, produce a new request with with. That is why building a new MessageCreateParams on every loop iteration is safe; earlier requests are not affected by the assistant turn added later.
The tests check the request JSON, that the same container is used after pause_turn, the anthropic-beta header, and that the files are downloaded and deleted:
Passed! - Failed: 0, Passed: 49, Skipped: 0, Total: 49, Duration: 77 ms - Bookstore.Api.Tests.dll (net10.0)
Security: Token, Sandbox and Skill
The scope of the MCP token. According to the documentation, running the OAuth flow and refreshing the token is the API user's job. For Bookstore that means a short-lived token that can only read stock. The token goes to Anthropic with every request and from there to the MCP server; the least-privilege rule from the fourth post of the LLM Application Security series applies directly. The allowlist is a second layer, not a replacement for authorization.
Data retention. The MCP connector, code execution, Files API and Skills pages all list these features as outside ZDR. Tool definitions and results shared with the MCP server follow the standard retention policy. Keeping personal data out of these paths is therefore a design decision.
The limits of the sandbox. With the internet off, code in the sandbox cannot send data out; but the file it produces and the answer text come back to you. Do not trust the Excel file's content before distributing it to users; the "LLM output is untrusted input" rule from the fifth post of the LLM Application Security series applies to files as well.
Alternative: Connecting MCP on Your Own Side
There are cases where the MCP connector is not enough: a local stdio server, MCP prompts or resources, or wanting to keep the connection inside your own network. For this the documentation points to a separate Anthropic.Mcp package in C#; it is used together with the official ModelContextProtocol package and connects MCP tools to the SDK's tool runner with helpers such as BetaMcp.ListToolsAsync(mcpClient). On NuGet, as of 15 November 2026, there are Anthropic.Mcp 1.0.0 and ModelContextProtocol 2.4.0. The second route is the AsIChatClient() bridge we will see in the eleventh post: take Claude as an IChatClient and connect the MCP C# SDK's tools through Microsoft.Extensions.AI. On this route tool calls leave from your own process and the token never goes to Anthropic.
In Production
Pin the skill version. latest is convenient in development; in production give a dated version for Anthropic skills and a specific skver_ id for your own. If the report looks different one day, you will want to know why.
Watch execution time. Used on its own, code execution is billed for at least 5 minutes. For a report that runs once a month that is negligible; for an assistant that turns on code execution for every request, the free hours can run out quickly. usage.server_tool_use in the response gives the number of code execution requests.
Catch the expired container. According to the documentation, sending a request to an expired container returns an error; in that case you start again without the container parameter.
Track MCP calls from the response. The Claude API makes the call, not Bookstore.Api, so that HTTP request is not in Bookstore.Api's log. You read what happened from the mcp_tool_use and mcp_tool_result blocks in the response; log the result block's is_error field and watch it together with the MCP server's own logs.
Common Pitfalls
Sending only mcp_servers with the MCP connector and forgetting the mcp_toolset. A validation error.
Trying to connect an MCP server on localhost. The server must be reachable from the internet over HTTPS.
Sending a skill without the code execution tool. Skills run in that container.
Having the file written outside $OUTPUT_DIR. The file stays in the container and no file_id comes back.
Not sending the container id on pause_turn. A new container, and lost files.
Assigning null to an optional SDK field, or changing a list after assigning it. The first sends null, the second does not send the change.
A Small Experiment
★☆☆ Add an XlsxSkillVersion to SalesReportOptions; when empty, latest goes out, otherwise that dated version. Check the version value in the JSON in a test.
★★☆ Add cache_control to the mcp_toolset in BuildRequest and turn on automatic caching as in the seventh post. In a two-turn test, confirm that the tools and container parts stay byte-identical.
★★★ Add the expired-container case to the loop: if the API returns an error, start once more without the container parameter. Add an error response to the fake handler, test this path, and see which SDK exception arrives.
In this post we extended Claude with capabilities that run within a single request. In the tenth post we go one step further: Claude Managed Agents, where Anthropic manages the agent loop, the container and the session history. We will look at the agent, environment and session concepts and at when to choose it.