Docs

ChartCraft MCP server

Forge designed Power BI projects from Claude Code, Cursor, VS Code or any MCP client. Bind them to your own semantic model by naming its tables and measures. Your data never leaves your machine.

1. What it is

The same Forge that runs at chartcraft.io/studio/forge, exposed to agents through the Model Context Protocol. One remote server, four tools, your ChartCraft account behind an API key. A forged report costs 30 tokens, exactly like in the app. Works on Free and Pro.

  • •Output: a real Power BI project (.pbip), PBIR report plus TMDL model, delivered as a zip through a signed link valid 24 hours.
  • •Same catalogue as the app: 18 palettes, 12 layouts, KPI card languages, native and Deneb charts, Materials imagery.
  • •Bound mode: name your tables, columns and measures, get a report-only project that reads your own model in Desktop.

2. Get a key

Sign in, open Settings, Account tab, then API keys. Name the key after where it lives (a laptop, an agent) and copy it: it is shown once. Up to 3 active keys, revoke any of them at any time. A key acts as you: same plan, same tokens.

3. Install

Keep the key in an environment variable or a masked prompt, never in a repository. The transport is Streamable HTTP, stateless, with the key as a bearer header. Claude Code, one line:

Claude Code
claude mcp add --transport http chartcraft https://chartcraft.io/api/mcp --header "Authorization: Bearer $CHARTCRAFT_API_KEY"

Cursor, in ~/.cursor/mcp.json (keep this file out of git):

Cursor
{
  "mcpServers": {
    "chartcraft": {
      "url": "https://chartcraft.io/api/mcp",
      "headers": { "Authorization": "Bearer cc_live_..." }
    }
  }
}

VS Code, in .vscode/mcp.json, the key asked once and stored masked:

VS Code
{
  "inputs": [
    { "type": "promptString", "id": "chartcraft-key", "description": "ChartCraft API key", "password": true }
  ],
  "servers": {
    "chartcraft": {
      "type": "http",
      "url": "https://chartcraft.io/api/mcp",
      "headers": { "Authorization": "Bearer ${input:chartcraft-key}" }
    }
  }
}

Codex, in ~/.codex/config.toml, the key read from your environment:

Codex
[mcp_servers.chartcraft]
url = "https://chartcraft.io/api/mcp"
bearer_token_env_var = "CHARTCRAFT_API_KEY"

Claude Desktop connects to remote servers with OAuth only, which this server does not offer yet. Bridge it through mcp-remote, which runs locally and sends the header (Node.js required), in claude_desktop_config.json:

Claude Desktop
{
  "mcpServers": {
    "chartcraft": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://chartcraft.io/api/mcp", "--header", "Authorization:${AUTH_HEADER}"],
      "env": { "AUTH_HEADER": "Bearer cc_live_..." }
    }
  }
}

claude.ai custom connectors also require OAuth: use Claude Code or Claude Desktop for now. No agent at hand? The same endpoint answers plain JSON-RPC:

curl
curl -X POST https://chartcraft.io/api/mcp \
  -H "Authorization: Bearer $CHARTCRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_balance","arguments":{}}}'

4. The tools

ToolWhat it doesCost
forge_reportCompiles a project from the same options as the app, plus an optional binding to your model. Returns a download link, tokens left, warnings and next steps. With dry_run: true, returns the page plan (cards, charts, slicers) without compiling.30 tokens, dry run free
list_forge_optionsThe catalogue: palettes with their Pro flag, layouts, industries, every option with a one-line meaning, chart ids per shape family, the binding contract with an example, costs. Ask for one section to keep it short.Free
suggest_bindingDrafts the binding from the declarations of your model (table, column and measure names, types, relationships, never DAX), the same draft the Forge screen makes, plus the guessed industry. Give it a binding as well and it lists every name that does not exist in your model.Free
get_balancePlan, tokens left, and how many reports that buys.Free

The three read tools are annotated read-only. forge_report is the only one that spends tokens: it reserves them before compiling and gives them back if the build or the delivery fails, so a failed call costs nothing.

5. A first run

In Claude Code, this is enough:

prompt
Check my ChartCraft balance, then forge a 3-page retail report in the ivory-gold palette,
classic layout, signature cards. Dry run first, then compile and give me the download link.

The agent calls get_balance, list_forge_options, forge_report with dry_run to read the plan, then forge_report for real. Download the zip, unzip it, open the .pbip in Power BI Desktop (free), click Refresh once to load the sample data.

6. Bind the report to your model

Give forge_report a binding: the names of the tables, columns and measures the report should read. You get a report-only project that points to your .SemanticModel folder. Every card, chart and slicer reads your own measures and your own data; your model is never received nor modified.

Where the names are: your project saved as .pbip contains <Model>.SemanticModel/definition/tables/*.tmdl. An agent with file access reads them locally and sends their declarations to suggest_binding, which drafts the period, the segment, the KPI measures and checks every name. It skips hidden measures and reads relationships.tmdl to know which tables filter your facts:

_Measures.tmdl, DimDate.tmdl
table _Measures
	measure 'Retention Rate' = 1 - DIVIDE([Voluntary Departures] * 12, [Headcount Start])
		formatString: 0.0%

table DimDate
	dataCategory: Time
	column YearMonth
		dataType: string

Already have a report on that model? Send its visuals too (the fields each visual.json binds, names only). The binding then follows what your report shows: its cards become the KPIs, its charts give the measures and the period axis, its slicers the segment and the range. Its tables, matrices and scatter charts are redrawn in the forged report with the same fields, in the style of its cards (Signature, Editorial or Cockpit). Maps and custom visuals stay in your report. The forged report sits next to yours and never takes its name.

The call, with a few of those names:

forge_report arguments
{
  "name": "Employee Retention Board",
  "palette": "ivory-gold",
  "industry": "human-resources",
  "pageCount": 2,
  "filters": "segment",
  "binding": {
    "modelFolder": "Employee_Retention.SemanticModel",
    "period": {
      "entity": "DimDate",
      "name": "YearMonth"
    },
    "segment": {
      "entity": "DimDepartment",
      "name": "DepartmentName",
      "iconColumn": "IconURL",
      "values": [
        "Engineering",
        "Sales",
        "Operations",
        "Marketing",
        "HR",
        "Finance"
      ]
    },
    "kpis": [
      {
        "measure": {
          "entity": "_Measures",
          "name": "Retention Rate",
          "label": "Retention rate",
          "formatString": "0.0%"
        },
        "higherIsBetter": true,
        "cumulative": false,
        "delta": "Retention Variation MoM",
        "target": "Retention Target",
        "lastMonth": "Retention Rate Prev Year"
      },
      {
        "measure": {
          "entity": "_Measures",
          "name": "Turnover Rate",
          "formatString": "0.0%"
        },
        "higherIsBetter": false,
        "cumulative": false,
        "delta": "Turnover Variation MoM"
      },
      {
        "measure": {
          "entity": "_Measures",
          "name": "Engagement %",
          "label": "Engagement",
          "formatString": "0.0%"
        },
        "higherIsBetter": true,
        "cumulative": false,
        "delta": "Engagement Variation MoM",
        "target": "Engagement Target"
      },
      {
        "measure": {
          "entity": "_Measures",
          "name": "Headcount End",
          "label": "Headcount",
          "formatString": "#,0"
        },
        "higherIsBetter": true,
        "cumulative": true
      },
      {
        "measure": {
          "entity": "_Measures",
          "name": "New Hires",
          "formatString": "#,0"
        },
        "higherIsBetter": true,
        "cumulative": true
      },
      {
        "measure": {
          "entity": "_Measures",
          "name": "Avg Tenure Months",
          "label": "Avg tenure (months)",
          "formatString": "0.0"
        },
        "higherIsBetter": true,
        "cumulative": false
      }
    ],
    "chartMeasures": [
      {
        "entity": "_Measures",
        "name": "Headcount End"
      },
      {
        "entity": "_Measures",
        "name": "New Hires"
      },
      {
        "entity": "_Measures",
        "name": "Departures"
      }
    ],
    "header": {
      "subtitleMeasure": {
        "entity": "_Measures",
        "name": "Dashboard Subtitle"
      }
    }
  }
}
  • •Every field carries its table. In this version a KPI card reads one table: its measure, delta, target and last period share it.
  • •On a calendar that spans several years, use a year-month column such as YearMonth (2026-01). A month-name column adds up every January of every year. Ordering comes from your model's Sort by column.
  • •A second slicer (range) filters by itself when its table is related to your facts. A period picker without relationship needs its gate measure (range.gate, like Is In Selected Period): charts on the period axis then follow it, cards keep the whole period.
  • •Give the segment its values (2 to 8 exact names) for sized tiles and one colour per value.
  • •Deneb charts and the detailed trend need measures your model may not have: the result names every degradation in its warnings. A dry run is free and shows them first.
  • •Extract the zip into the folder that already contains your .SemanticModel, so the new .Report sits next to it, then open the .pbip. Your own report folder is untouched.

7. Save your report as .pbip

Bound mode needs a Power BI project on disk, not a .pbix. A .pbix keeps the model in a sealed binary; the conversion only happens in Power BI Desktop:

  1. File, Options and settings, Options, Preview features.
  2. Tick Power BI Project (.pbip) save option and Store reports using enhanced metadata format (PBIR). On recent builds both are on by default.
  3. Restart Desktop and open your .pbix.
  4. File, Save as, choose Power BI project files (*.pbip), an empty folder. Accept the upgrade to PBIR if asked.
  5. You now have <Name>.pbip, <Name>.Report and <Name>.SemanticModel. The model folder is the one to name in modelFolder.
  6. Keep that folder: the forged report is extracted next to it.

8. What leaves your machine

Sent to ChartCraftNever sent
The forge options you pick. In bound mode, the names of the tables, columns and measures you list, their format strings, and your model folder name. With an existing report, the fields each visual binds, how a column is summarized, and the header names you gave fields in those visuals.Data rows, DAX expressions, Power Query (M) partitions, the cache, visual titles, filter values, formatting, images, anything else in the .SemanticModel and .Report folders.

Names can be sensitive in some companies. The generated zip, which carries the names you listed, is kept in a private bucket under a link only your agent receives, then deleted after 24 hours. Nothing else from the call is stored. Generate and Forge never see your data; that stays true with the MCP server.

9. Next to Microsoft's report skill

Microsoft ships an open skill and a CLI for agents editing an existing PBIR report: grammar lookup, validation, packing to Fabric, Desktop screenshots on Windows. ChartCraft does the other half: it designs a new report and binds it to your model, deterministically, from any machine. They edit what exists; we forge what does not. Run both: forge with ChartCraft, validate with powerbi-report-author validate, refine in Desktop.

10. Limits and errors

  • •3 forged reports per minute per key, 30 read calls per minute. Tokens remain the real ceiling: 120 a month on Free, 1200 on Pro.
  • •Download links expire after 24 hours. Forge again if you missed the window.
  • •Premium palettes and your Custom Studio materials need Pro, in the app and here alike.
  • •A refused call is a tool result flagged as an error with a stable code in _meta.error_code: insufficient_tokens, pro_required, rate_limited, asset_not_found, compile_failed. An argument that does not match the schema is refused earlier, by the protocol: a JSON-RPC error -32602 that names the field.
  • •A missing or revoked key answers 401. The server is stateless: no session to keep, nothing to reconnect.
  • •Reports on a live connection (a thin report on a shared or Fabric model, without a local .SemanticModel folder) cannot be bound yet.

If the bound report does not open as expected

  • •Visuals show no data: the model has no cached data yet (a folder cloned from git, for instance). Click Refresh once.
  • •A visual shows an error: a name in the binding does not exist in the model. Check its spelling and table, then forge again.
  • •Desktop refuses to open: close any other Desktop window on the same model, and check that the zip sits next to the .SemanticModel folder it names.
  • •To go back to a single file, use File, Save as, .pbix in Desktop; Publish sends the report and the model together.

Questions or a report that does not open as expected: write to us.

With your consent, we use Google Analytics cookies to measure audience. Cookies needed for sign-in and payment are always on. Learn more