McpCarbonServer 0.6.0-alpha.1
dotnet tool install --global McpCarbonServer --version 0.6.0-alpha.1
dotnet new tool-manifest
dotnet tool install --local McpCarbonServer --version 0.6.0-alpha.1
#tool dotnet:?package=McpCarbonServer&version=0.6.0-alpha.1&prerelease
nuke :add-package McpCarbonServer --version 0.6.0-alpha.1
mcp-carbon-server
An MCP server that gives an LLM client real greenhouse gas accounting: Scope 1/2/3 calculation over a versioned, source-cited emission factor catalog, with unit conversion and AR5/AR6 GWP set selection.
Calculation is not done here. It is done by GhgAccounting, a standalone library aligned to the GHG Protocol and ISO 14064-1. This repository is the protocol surface over it: tool definitions, schemas, transport and packaging. The split is deliberate — the accounting logic has to be usable from an ERP or a batch job, not only from a chat client.
Why it exists
An LLM asked to work out a company's footprint will produce a number. Without tools it produces that number from memory: an emission factor it half-remembers, no version, no geography, no citation. The figure looks like every correct figure and cannot be audited.
This server replaces that with a lookup against a compiled catalog. Every result carries the factor it used, the dataset that published it, the publication year, and whether those numbers have been verified against the cited source.
The provenance in that answer is not the model being careful. It is what the tool returned, because a result that cannot be traced to a dataset is not something this server will produce.
The same question in Turkish, resolving to the same factor and the same figure. Nothing in the server is localised — factor ids, dataset names and verification status are what the publishers wrote, and the answer is rendered in whatever language the conversation is in. Which is the useful property: a Turkish disclosure still cites DESNZ and Eurostat by the names an auditor can look up.
The recording is generated from demo/demo.tape against the published
tool — vhs demo/demo.tape re-renders it. The tape is the source and the GIF is a build
artifact, so when the tool surface changes the demo is regenerated rather than re-recorded,
and it cannot quietly drift into showing a command that no longer exists.
demo/carbon is the small helper the recording drives: it performs a handshake, makes one
tool call and prints the structured result, so the tools can be shown without a chat client
in the frame. It is a demo aid rather than a client — a real MCP host keeps one server
process for the whole session.
Tools
| Tool | What it does |
|---|---|
list_factor_sets |
Datasets compiled into this build, with publisher, coverage and verification status |
search_emission_factors |
Find factor ids by activity wording, scope, region or dataset; reports how many matched as well as how many it returned |
calculate_emissions |
Apply one factor to one activity figure; returns CO2e with per-gas breakdown and provenance |
build_inventory |
Aggregate many lines into scope 1/2/3 totals, scope 2 both ways, scope 3 by category |
convert_units |
Convert between units of the same physical dimension |
Each is annotated read-only, idempotent and closed-world — they are pure functions over a catalog compiled into the binary, reading nothing outside the process and writing nothing at all. A host can act on that: Claude Desktop groups them as read-only and offers to allow them without prompting per call.
Resources
Attachable context, projected from the compiled catalog rather than written out, so a resource cannot drift from what the tools compute.
| URI | What it is |
|---|---|
carbon://factor-sets |
Every dataset in this build, with publisher, coverage, licence and verification status |
carbon://factor-sets/{setId} |
One dataset in full, including every factor it publishes |
carbon://gwp/{gwpSet} |
The global warming potentials actually compiled in for one assessment report |
The last one is worth attaching when a disclosure has to state which potentials it used: the answer is a property of the numbers shipped, not of what the report says in general.
Prompts
| Prompt | What it frames |
|---|---|
ghg_inventory_intake |
Collecting activity data and turning it into a scope 1/2/3 inventory |
disclosure_review |
Checking draft figures against what a disclosure has to carry, reporting gaps rather than filling them |
Install
Needs the .NET 10 runtime — both Microsoft.NETCore.App and Microsoft.AspNetCore.App,
the second because the same executable also serves HTTP. The .NET 10 SDK includes both.
Published as a .NET global tool. Releases are pre-release for now, so the flag is required:
dotnet tool install -g McpCarbonServer --prerelease
Then point an MCP host at the mcp-carbon-server command.
Claude Desktop
Current versions manage local MCP servers as extensions. Hand-editing
claude_desktop_config.json no longer works — the app rewrites that file on quit and drops
an mcpServers key it did not put there, so the server never starts and nothing explains
why.
Create a folder containing a single manifest.json:
{
"manifest_version": "0.3",
"name": "mcp-carbon-server",
"display_name": "Carbon",
"version": "0.4.0",
"description": "GHG Protocol greenhouse gas accounting over a source-cited emission factor catalog.",
"author": { "name": "Your Name" },
"server": {
"type": "binary",
"entry_point": "/absolute/path/to/mcp-carbon-server",
"mcp_config": {
"command": "/absolute/path/to/mcp-carbon-server",
"args": []
}
}
}
Then Settings → Extensions → Install Unpacked Extension and pick that folder.
Two things that will otherwise cost an evening:
Use an absolute path. On macOS a GUI application does not inherit your shell's PATH,
so ~/.dotnet/tools is not on it and a bare mcp-carbon-server is not found. which mcp-carbon-server gives you the path to paste.
If the extension installs but reports it cannot connect, check
~/Library/Logs/Claude/mcp-server-*.log. A second .NET installation is the usual cause: the
launcher resolves the runtime from the default location rather than from whichever dotnet
your shell uses, and if that one is older the process exits immediately. Point it at the
right root by adding to mcp_config:
"env": { "DOTNET_ROOT": "/opt/homebrew/opt/dotnet/libexec" }
That path is for a Homebrew install; dotnet --info reports the correct root for yours.
Transports
The same server serves both. Which one you want depends on who is starting it.
stdio is the default and needs no arguments. A desktop host launches the process and owns it; one client, one process, no ports.
Streamable HTTP is --http, for a deployment that serves clients it did not start:
mcp-carbon-server --http # http://localhost:5000
mcp-carbon-server --http --urls http://0.0.0.0:8080 # or ASPNETCORE_URLS
MCP is served at /mcp. /health answers without opening a protocol session, which is
what a container orchestrator needs — every MCP route expects a handshake first.
The legacy SSE transport is deliberately not mapped. The SDK marks it obsolete: it has no request backpressure and is meant for completely trusted clients in isolated processes. Enabling it to widen client compatibility would trade a real property of a network-facing server for reach it does not need.
Tools, resources, prompts and server identity are registered once and shared by both transports, and a test asserts the two surfaces match — a capability available over one and not the other is a difference nobody could explain from the outside.
Container
docker run --rm -p 8080:8080 ghcr.io/agirgol/mcp-carbon-server:latest
curl -s localhost:8080/health
The image serves the HTTP transport only. stdio is for a desktop host that launches the process and owns its stdin and stdout, which is not something a container gives you.
It runs as a non-root user, carries a HEALTHCHECK against /health, and is published for
linux/amd64 and linux/arm64. The publish is deliberately not trimmed: tools, resources
and prompts are discovered by reflection over the assembly, and a trimmer has no way to see
that — trimming would produce an image that starts cleanly and serves an empty tool list.
Because the server keeps no state between requests, instances can sit behind a load balancer without session affinity.
Design notes
stdout belongs to the protocol. Under the stdio transport, stdout carries JSON-RPC frames and nothing else. Every log line goes to stderr, and the generic host's default console logger is removed at startup rather than reconfigured — a single stray write desynchronises the framing and the host drops the server without surfacing an error.
Units are carried, not assumed. Activity data is accepted in any unit measuring the same physical quantity as the factor's denominator and converted. A unit from another dimension is rejected instead of being coerced through an assumed density or calorific value.
Scope 2 is reported twice. Location-based and market-based are separate figures under
the GHG Protocol, and only one of them belongs in a given total. build_inventory returns
both and asks which the headline total should use; requesting a method no line reports is
an error rather than a total that quietly omits scope 2.
Biogenic carbon sits outside the total. It is disclosed separately, as the standard requires, instead of being folded into the scopes.
Verification status travels with the number. Factor sets carry a status, and it is returned on every result. A figure derived from an unverified set is not a disclosure and the response says so.
Results are data, not prose. Every tool publishes an output schema and returns structured content, so a client gets a validated object rather than a string to parse. The tools are annotated read-only and idempotent — they are pure functions over a catalog compiled into the binary, reading nothing outside the process — so a client can call them without an approval prompt.
Building from source
The server depends on GhgAccounting. When a checkout of
carbon-accounting-dotnet sits
beside this repository, the build references that project directly, so library changes
show up on the next build with no pack/restore cycle. Otherwise it falls back to the
published package. Neither case needs a flag set.
git clone https://github.com/agirgol/mcp-carbon-server
git clone https://github.com/agirgol/carbon-accounting-dotnet # optional
dotnet build McpCarbonServer.slnx
To build against the published package while the sibling checkout is present — worth doing before tagging a release, since the two do not compile in the same factor catalog:
dotnet build McpCarbonServer.slnx -p:UseLocalGhgAccounting=false
Tests
dotnet test McpCarbonServer.slnx
The tests launch the built server as a child process and speak MCP to it — over stdio and over HTTP — rather than calling the tool methods in-process. That exercises the shipped executable, the transports, the generated schemas and the exception-to-error mapping in one path, and makes stdout discipline self-testing: a stray write to stdout under stdio desyncs the framing, the client fails to parse, and the whole suite goes red at once.
There is no coverage figure, and that is the cost of the choice above. A collector instruments the test process while the code under test runs in another one, so it reports zero packages — which presents as a line rate of 1. A hundred percent that measures nothing is worse than no number.
Licence
MIT. Emission factor data carries the licence of its publisher; see the NOTICE file in
carbon-accounting-dotnet.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0-alpha.1 | 81 | 8/19/2026 |
| 0.5.0-alpha.1 | 76 | 8/18/2026 |
| 0.4.0-alpha.1 | 78 | 8/18/2026 |
| 0.3.0-alpha.1 | 79 | 8/17/2026 |
| 0.2.0-alpha.1 | 87 | 8/17/2026 |
| 0.1.0-alpha.1 | 88 | 8/17/2026 |