Turn any website or API into structured JSON with LLM-authored declarative scraping configs.
Model Context Protocol (MCP) Server: io.github.PxyUp/fitter
The MCP server “io.github.PxyUp/fitter” turns a website or API into structured JSON. It uses LLM-authored declarative scraping configurations to define how content should be extracted and output in JSON format.
🛠️ Key Features
Converts “any website or API” into structured JSON
Uses LLM-authored declarative scraping configs
🚀 Use Cases
Scrape data from websites into structured JSON
Transform API-derived content into structured JSON using declarative configurations
⚡ Developer Benefits
Produces structured JSON output from web or API sources
Uses declarative scraping configurations authored with LLM assistance
⚠️ Limitations
Source material does not specify supported content types, rate limits, authentication handling, or error behavior
Fitter turns any website or API into structured JSON — declaratively. One JSON/YAML config describes where the data lives (HTTP request, headless browser, file, static value) and what to extract (JSON paths, CSS selectors, XPath). No code, no brittle scraping scripts.
🚀 Try it in your browser — the real engine compiled to WebAssembly: live examples, a visual config builder, no install.
Because configs are plain data, LLMs can author them. The built-in MCP server lets Claude Code, Claude Desktop, or any MCP client write and run scraping pipelines on your machine, on demand:
"Get the top 5 HackerNews stories with titles and scores" → the model authors a fitter config, validates it, runs it locally, and gets clean JSON back.
One engine, five ways to use it:
🤖 Fitter MCP
MCP server exposing fitter to Claude Code, Claude Desktop and any MCP client
🧠 Fitter Agent
AI-powered CLI: natural language → config → executed result
🖥 Fitter CLI
run configs locally for test/debug/home usage
📦 Fitter Lib
embed the engine in your own Go program
⚙️ Fitter
long-running service mode with scheduling & notifications
Why fitter for AI agents?
Declarative & auditable — the agent produces a config you can read, save and re-run, not throwaway code
Local-first — all fetching happens on your machine; no third-party scraping API, no keys, no per-request billing
Batteries included — HTTP client, headless browser (Playwright/Chromium/Docker), JSON/HTML/XML/XPath/PDF parsing, pagination, cached references, host rate-limits — in a single static binary
Reusable — what the agent authored today becomes tomorrow's cron job or service config
fitter demo — declarative config to structured JSON
How to use Fitter_MCP
Fitter MCP is a Model Context Protocol server (stdio transport) which lets any MCP client — Claude Code, Claude Desktop, IDE assistants, custom agents — run Fitter configs and get structured JSON back.
Quick start (Claude Desktop — one click)
Download fitter-mcp-<os>-<arch>.mcpb from the release page and open it — Claude Desktop installs the server automatically.
Quick start (Claude Code)
bash
# 1. get the binary: download fitter_mcp_<version>-<os>-<arch> from the release page# https://github.com/PxyUp/fitter/releases — or build it from source:
go build -o fitter_mcp ./cmd/mcp
# 2. register it once, available in every project
claude mcp add fitter -s user -- "$(pwd)/fitter_mcp"
Then just ask:
Get the top 5 HackerNews stories with titles and scores using fitter
The model calls fitter_config_reference, authors a config, optionally checks it with fitter_validate_config and executes it via fitter_run — all data fetching happens locally on your machine. For a ready-made pipeline try examples/config_morning_briefing.json:
Run examples/config_morning_briefing.json with fitter and give me the briefing
Run a Fitter config passed inline (JSON or YAML string) and return the extracted data as JSON. Accepts an optional input value available in the config via {{{FromInput=.}}} / {{{FromInput=json.path}}}
fitter_run_file
Same as fitter_run but reads the config from a local .json/.yaml file
fitter_run_url
Same as fitter_run but downloads the config from an HTTP(S) URL, e.g. a raw GitHub link
fitter_validate_config
Validate a config without executing it (structure, response_type, connector data source, model). Useful while iterating on a config
fitter_config_reference
Return a condensed reference of the whole config format (connectors, parsers, model/field schema, placeholders, notifiers, references, limits) with working examples, so the model can author configs without external docs
The reference is also exposed as MCP resource fitter://config-reference for clients that support resources.
The config format is exactly the same as for Fitter_CLI: a top-level object with item (required), limits and references. Notifiers work too (the result is additionally pushed to http/telegram/redis/file/console); trigger_config and http_server are service-mode only and are ignored in MCP calls.
Remote / hosted mode (streamable HTTP)
By default fitter_mcp talks stdio. Pass --http to serve the streamable HTTP transport instead — for a shared team server, a container, or any remote deployment:
bash
# serve MCP at http://<host>:8080/mcp (health probe at /healthz)
FITTER_MCP_AUTH_TOKEN=my-secret fitter_mcp --http :8080
# register the remote endpoint in Claude Code
claude mcp add --transport http fitter http://localhost:8080/mcp --header "Authorization: Bearer my-secret"
FITTER_MCP_AUTH_TOKEN — when set, every /mcp request must send Authorization: Bearer <token>; without it the endpoint is unauthenticated, so bind to localhost or put it behind a proxy
--stateless (env FITTER_MCP_STATELESS=true) — no per-session state, so replicas can sit behind a load balancer without sticky sessions
The server shuts down gracefully on SIGINT/SIGTERM.
Docker
A slim multi-arch image (linux/amd64 + linux/arm64) ships with every release:
bash
# hosted HTTP mode
docker run --rm -p 8080:8080 \
-e FITTER_MCP_HTTP_ADDR=:8080 \
-e FITTER_MCP_AUTH_TOKEN=my-secret \
ghcr.io/pxyup/fitter-mcp:latest
# or stdio mode, spawned by the MCP client
claude mcp add fitter -s user -- docker run --rm -i ghcr.io/pxyup/fitter-mcp:latest
The image contains only the fitter binary and CA certificates: server/static/file connectors work, browser connectors (chromium/docker/playwright) do not — use a release binary on a host with a browser for those.
Environment variables
FITTER_PLUGINS - string[""] - path for plugins folder, same as the --plugins flag of Fitter/Fitter_CLI
FITTER_MCP_HTTP_ADDR - string[""] - listen address for remote mode, same as --http
FITTER_MCP_AUTH_TOKEN - string[""] - bearer token protecting the HTTP endpoint
FITTER_MCP_STATELESS - bool[false] - stateless HTTP transport, same as --stateless
Recipes
Complete, tested configs showing the main patterns. All of them run unchanged via Fitter_MCP (fitter_run_file), Fitter_CLI or the library — more in examples/.
Scrape a page that has no API, enrich from one that does
GitHub trending has no official API — scrape the HTML for repo slugs (html_attribute reads the href), then fan each one out into the GitHub REST API with {PL}:
When array items are objects, the join key lives inside them — pull it out with {{{FromExp=...}}} (expr-lang over fRes, the current item). Book search → author details, search query supplied via input:
[{"title":"Dune","year":1965,"author":{"name":"Frank Herbert","born":"8 October 1920","died":"11 February 1986"}}]
Write results to a local file
The file_storage generated field turns fields into writes — top-5 crypto coins appended to a CSV, one row per item. Bare {{{json.path}}} placeholders read the current item; {HUMAN_INDEX} stamps the 1-based rank (items are processed in parallel, so appends land in completion order — sort by the rank column):
response_type: "pdf" turns any fetched PDF into a JSON document — {"text": "...", "pages": ["..."], "total_pages": N} — so regular JSON paths (text, pages.0) and expressions work on it. The Bitcoin whitepaper, page count plus a trimmed intro:
{"intro":"Bitcoin: A Peer-to-Peer Electronic Cash SystemSatoshi Nakamotosatoshin@gmx.comwww.bitcoin.orgAbstrac...","total_pages":9}
Way to collect information
Server - parsing response from some API's or http request(usage of http.Client)
Browser - emulate real browser using chromium + docker + playwright/cypress and get DOM information
Static - parsing static string as data
Format which can be parsed
JSON - parsing JSON to get specific information
XML - parsing xml tree to get specific information
HTML - parsing dom tree to get specific information
XPath - parsing dom tree to get specific information but by xpath
PDF - extracting text from PDF documents; the content is exposed as JSON {"text": "...", "pages": ["..."], "total_pages": N} so regular JSON paths like text or pages.0 work
Use lib.ParseCtx(ctx, ...) to pass a context.Context: cancelling it aborts in-flight fetches (HTTP requests, headless browsers, docker containers) and applies deadlines end-to-end. lib.Parse is equivalent to lib.ParseCtx(context.Background(), ...).
┌─────────────────────────────────────────────────────────────────┐
│ 1. User enters natural language request │
│ "Get top 5 HackerNews stories with titles and scores" │
│ ↓ │
│ 2. Claude returns a config in a schema-constrained response │
│ ↓ │
│ 3. Agent validates it; on failure the error is handed back │
│ to Claude to repair (up to 3 attempts) │
│ ↓ │
│ 4. Agent displays config and asks for confirmation │
│ ↓ │
│ 5. On confirmation, executes via lib.Parse() │
│ ↓ │
│ 6. Returns structured JSON result │
└─────────────────────────────────────────────────────────────────┘
Refining a config
The agent keeps the conversation, so after a config is generated you can just
say what to change instead of restating the whole request:
code
> Get top 3 HackerNews stories with titles and scores
refine> Only return 5 items and also include the article URL
Use new to forget the current config and start a fresh session.
Interactive REPL Commands
help - show help message
new/reset - forget the current config and start fresh
NullOnError[false] - if set to true then all errors a ignored
ResponseType - enum["HTML", "json", "xpath", "XML", "pdf"] - in which format data comes from the connector
Attempts - how many attempts to use for fetch data by connector
Url - define which address to request. Important: can be with inject of the parent value as a stringhttps://api.open-meteo.com/v1/forecast?latitude={{{latitude}}}&longitude={{{longitude}}}&hourly=temperature_2m&forecast_days=1
type ProxyConfig struct {
// Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example// `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128`// is considered an HTTP proxy.
Server string`json:"server" yaml:"server"`// Optional username to use if HTTP proxy requires authentication.
Username string`json:"username" yaml:"username"`// Optional password to use if HTTP proxy requires authentication.
Password string`json:"password" yaml:"password"`
}
Server - address with schema of proxy server. Also support formatting
Username - username for proxy(can be empty). Also support formatting
Password - password for proxy(can be empty). Also support formatting
BaseField - configuration of single/generated field
IsArray - bool[false] - force indicate that field is array(usable in case of model field with base field)
Example:
json
{"object_config":{}}
ObjectConfig
Configuration of the object and fields
go
type ObjectConfig struct {
Fields map[string]*Field `json:"fields" yaml:"fields"`
Field *BaseField `json:"field" yaml:"field"`
ArrayConfig *ArrayConfig `json:"array_config" yaml:"array_config"`
Condition string`json:"condition" yaml:"condition"`
}
Condition - optional condition expression evaluated against the source node before resolution; when false the whole object is omitted from the parent (fields are not resolved at all)
Config can be one of:
Fields - map of each field definition; key - field name, value - configuration
Field - used for element of array; fields which will be deserialized like basic type like "string", "int" and etc (used here for case array of basic types)
ArrayConfig - used for element of array; deserialization array of array
RootPath - selector for find root element of the array or repeated element in case of html parsing, size of array will be amount of children element under the root
Reverse - bool[false] - indicate that need use reverse iteration(n to 1)
LengthLimit - for define size of array only for generated(not working for static)
Condition - optional condition expression evaluated against the source node before resolution; when false the whole array is omitted from the parent
ItemCondition - optional condition expression evaluated against every built item (fRes - item value, fSrc - source element, fIndex - item index); items resolving to false are dropped from the array - declarative filtering. Not applied to static_array
Config can be one of:
ItemConfig - configuration of each element of the array
FieldType - enum["null", "boolean", "string", "int", "int64", "float", "float64", "array", "object", "html", "raw_string"] - static field for parse. Important: type html will only works from connector which return HTML (HTMLAttribute - have no effect in this case). Example
Path - selector(relative in case it is array child) for parsing
HTMLAttribute - extra value which have effect only in HTML parsing via goquery. Here you can specify which attribute need to be parsed.
Condition - optional condition expression evaluated against the extracted value (fRes/fResJson/fResRaw, fIndex; fSrc - the node the field was resolved from, siblings included); when false the field is omitted from the parent object/array instead of producing null. Evaluated before Generated, so a false condition also skips generated work (sub-requests, file downloads)
Important: by default "string" type trimmed and all special chars is replaced, if you need plain string use "raw_string"
Config can be one of or empty:
Generated - field can be generated one which custom configuration
FirstOf - first not empty resolved field will be selected
Examples
json
{"generated":{"uuid":{}}}
json
{"type":"string","path":"text()"}
Conditional fields
Every field can carry a condition - an expr-lang expression (predefined values). When it evaluates to anything except true the field is omitted from the output (the key/item disappears), not set to null. An invalid expression also omits the field and logs an error.
Where the condition is evaluated:
BaseField.condition - after extraction: fRes is the extracted value, fSrc the node the field was resolved from (its siblings included) - so fSrc.on_sale == true can gate a field on data you did not extract. A false condition skips generated work entirely (no sub-request, no file download)
ObjectConfig.condition / ArrayConfig.condition - before resolution: fRes/fSrc are the source node (parsed value for json, text content for html)
ArrayConfig.item_condition - against every built item: fRes is the item, fSrc the source element it was built from, fIndex its index; false items are dropped - declarative array filtering. Use fSrc to filter on source attributes without adding them to the output
Filter array items - fSrc.in_stock reads the source element (not extracted into the output), fRes.price the built item:
Generate random UUID V4 on the flight, can be used for generate uniq id
go
type UUIDGeneratedFieldConfig struct {
Regexp string`yaml:"regexp" json:"regexp"`
}
Regexp - provide matcher which can be used for get part of generated uuid
Static
Generate static field
go
type StaticGeneratedFieldConfig struct {
Type FieldType `yaml:"type" json:"type"`
Value string`json:"value" yaml:"value"`
Raw json.RawMessage `json:"raw" yaml:"raw"`
}
Type - enum["null", "boolean", "string", "int","int64","float","float64", "array", "object"] - type of the field
Value - string value of the field
Raw - pure json value of the field
Example
json
{"type":"int","value":"65"}
json
{"type":"array","value":"[65,45]"}
json
{"type":"array","raw":[65,45]}
Formatted Field Config
Generate formatted field which will pass value from parent base field
go
type FormattedFieldConfig struct {
Template string`yaml:"template" json:"template"`
}
Template - template in with placeholder {PL} where parent value will be injected like string
FileName - local file name for storing file. By default, it is try get FileName from header, after that from url. Important: can be with inject of the parent value as a string.
Path - local file parent directory for storing file. Default path it is process directory. Important: can be with inject of the parent value as a string
FileName - local file name for storing file. By default, it is try get FileName from header, after that from url. Important: can be with inject of the parent value as a string.
Path - local file parent directory for storing file. Default path it is process directory. Important: can be with inject of the parent value as a string
Result of the field will be local file path as string
Field can generate different types depends from expression
go
type CalculatedConfig struct {
Type FieldType `yaml:"type" json:"type"`
Expression string`yaml:"expression" json:"expression"`
}
Type - resulting type of expression\
Expression - expression for calculation (we use this lib for calculated expression)
Predefined values
FNull - alias for builder.Nullvalue
FNil - alias for nil
isNull(value T) - function for check is value is FNull
fRes - it is raw(with proper type) result from the parsing base field
fIndex - it is index in parent array(only if parent was array field)
fResJson - it is JSON string representation of the raw result
fResRaw - result in bytes format
fSrc - only in condition/item_condition expressions: the source node the value was resolved from (parsed value for json - siblings included, text content for html). Not available in calculated/formatted/notifier expressions
type PluginFieldConfig struct {
Name string`json:"name" yaml:"name"`
Config json.RawMessage `json:"config" yaml:"config"`
}
Name - name of the plugin(without extension just name)
Config - json config of the plugin
Model Field
Field type which can be generated on the flight by news model and connector
go
type ModelField struct {
// Type of parsing
ConnectorConfig *ConnectorConfig `yaml:"connector_config" json:"connector_config"`// Model of the response
Model *Model `yaml:"model" json:"model"`
Type FieldType `yaml:"type" json:"type"`
Path string`yaml:"path" json:"path"`
Expression string`yaml:"expression" json:"expression"`
}
Current time is: {PL} with token from TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}}
text
Current time is: {PL} with token from TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}}
text
TokenRef={{{RefName=TokenRef}}} and TokenObjectRef={{{RefName=TokenObjectRef token}}} Object={{{value}}} {PL} Env={{{FromEnv=TEST_VAL}}} {INDEX} {HUMAN_INDEX}
References
Special map which prefetched(before any processing) and can be user for connector or for placeholder
Can be used for:
Cache jwt token and use them in headers
Cache values
Etc
Reference
go
type Reference struct {
*ModelField
Expire *uint32`yaml:"expire" json:"expire"`
}
ModelField - is embedded struct, you can use same fields
Expire[sec] - duration when reference is expired after fetching. Not set => forever cached. Set to 0 => every time re-fetch. Set to n > 0 => cached for n second
{"references":{"TokenRef":{"expire":10,"connector_config":{"response_type":"json","static_config":{"value":"\"plain token\""}},"model":{"base_field":{"type":"string"}}},"TokenObjectRef":{"connector_config":{"response_type":"json","static_config":{"value":"{\"token\":\"token from object\"}"}},"model":{"object_config":{"fields":{"token":{"base_field":{"type":"string","path":"token"}}}}}}}}
Optional per-item config item.notifier_config which pushes the parse result somewhere after processing. The result is still returned as usual (CLI/MCP output, service logs); the notifier additionally delivers it. Works in Fitter (service mode), Fitter_CLI and Fitter_MCP.
Expression - optional expr-lang condition: notify only when it evaluates to true. The parse result is available as fRes (parsed value), fResRaw (raw bytes), fResJson (JSON string), e.g. len(fResRaw) > 0
Force - notify even if parsing finished with an error
SendArrayByItem - if the result is an array, send each element as a separate notification
Template - optional template applied to the result before sending, placeholders allowed
Destination - exactly one of console, telegram_bot, http, redis, file