Skip to main content
Glama
AlonsoAlviraa

structured-extract-mcp

structured-extract

CI

Extract a typed record from unstructured text, check it against a Pydantic model, and try again with the validation error if it does not match.

Source: https://github.com/AlonsoAlviraa/structured-extract-mcp

This repository is a small public example of two things: a provider interface around structured LLM output, and a minimal MCP server. It is meant to be read in one sitting.

Author: Alonso Alvira Ballano. License: MIT.

What it does

You give it a schema name and a piece of text (an invoice email, a job posting, a support ticket). It asks a provider for JSON, validates that JSON with Pydantic, and returns the model. If validation fails, it sends the error back to the provider and tries again, up to a fixed number of attempts. If the attempts run out, it raises. It does not fill in an empty object and call that success.

The default provider is offline. It does not call a model and it does not need an API key. A second provider speaks the OpenAI chat-completions API, so the same client can point at OpenAI, NVIDIA NIM, Ollama, or another compatible server.

Related MCP server: genpark-structured-extract-schema-validator-skill

60-second quickstart

Requires Python 3.11+ and uv. Commands assume the repository root.

uv sync
uv run extract --list-schemas
uv run extract --schema invoice examples/invoice.txt
uv run extract --schema job_posting examples/job_posting.txt
uv run extract --schema support_ticket examples/support_ticket.txt

--list-schemas prints:

invoice
job_posting
support_ticket

extract --schema invoice examples/invoice.txt prints the validated object. Logs go to stderr. The default level is warning, so a successful run writes only JSON to stdout. Excerpt (the file has three line items):

{
  "vendor": "Northwind Supply Co.",
  "invoice_number": "INV-2026-1042",
  "issued_on": "2026-03-12",
  "currency": "USD",
  "total": "182.52",
  "payment_terms": "Net 30"
}

Money is a decimal string ("182.52"), not a JSON number.

The same command with examples/job_posting.txt as if it were an invoice exits 1 and prints the reason. The offline provider does not guess missing fields.

Without uv:

python -m venv .venv
source .venv/bin/activate
pip install -e .
extract --schema invoice examples/invoice.txt

Using a real model

The offline provider stays selected until you opt in. A missing key does not fall back to it.

export STRUCTURED_EXTRACT_PROVIDER=openai
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini
# Optional. Include the /v1 prefix your server expects.
export OPENAI_BASE_URL=https://api.openai.com/v1
uv run extract --schema invoice examples/invoice.txt

Variable

Default

Meaning

STRUCTURED_EXTRACT_PROVIDER

fake

fake or openai

OPENAI_BASE_URL

https://api.openai.com/v1

Base URL. The client posts to {base_url}/chat/completions

OPENAI_API_KEY

unset

Required when the provider is openai. Servers that ignore the key still need a non-empty value

OPENAI_MODEL

gpt-4o-mini

Model name sent in the request

STRUCTURED_EXTRACT_RESPONSE_FORMAT

json_schema

json_schema, json_object, or none

STRUCTURED_EXTRACT_TIMEOUT

30

Seconds for one request

STRUCTURED_EXTRACT_MAX_ATTEMPTS

3

Validation attempts, including the first

STRUCTURED_EXTRACT_LOG_LEVEL

WARNING on the MCP server

Standard library level. The CLI uses warning, or debug with --verbose

json_schema sends the Pydantic schema as OpenAI strict structured output (response_format.type = json_schema, strict: true). Some local servers only accept {"type": "json_object"} or reject response_format entirely. Set STRUCTURED_EXTRACT_RESPONSE_FORMAT to match the server. The client does not downgrade on its own: if the server returns an error, that error is raised.

Ollama's OpenAI-compatible endpoint is typically:

export STRUCTURED_EXTRACT_PROVIDER=openai
export OPENAI_BASE_URL=http://127.0.0.1:11434/v1
export OPENAI_API_KEY=ollama
export OPENAI_MODEL=llama3.1
export STRUCTURED_EXTRACT_RESPONSE_FORMAT=json_object

CLI

extract --schema invoice examples/invoice.txt
extract --schema invoice -          # stdin
extract --list-schemas
extract --provider openai --timeout 20 --max-attempts 2 --schema invoice note.txt
extract --verbose --schema invoice examples/invoice.txt

Exit codes: 0 validated JSON on stdout, 2 bad arguments or configuration (unknown schema, missing file, missing API key), 1 the provider failed or validation never passed.

MCP server

The server uses the official mcp Python SDK (MCPServer in mcp 2.x) and stdio. It exposes two tools:

  • list_schemas — schema names

  • extract(schema, text) — validated JSON text, or a tool error

Stdout is the protocol. The process logs to stderr.

uv run structured-extract-mcp

That process waits for an MCP client. Cursor (~/.cursor/mcp.json) and Claude Desktop (claude_desktop_config.json) can start it:

{
  "mcpServers": {
    "structured-extract": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/this/repo", "run", "structured-extract-mcp"],
      "env": {
        "STRUCTURED_EXTRACT_PROVIDER": "fake"
      }
    }
  }
}

Point the same block at a model by setting STRUCTURED_EXTRACT_PROVIDER to openai and adding OPENAI_API_KEY, OPENAI_BASE_URL, and OPENAI_MODEL. Paths must be absolute. uv --directory is there so the server can be started from another working directory.

Architecture

flowchart TD
  cli[CLI extract]
  mcp[MCP stdio tools]
  ext[Extractor]
  reg[Schema registry]
  prov{Provider}
  fake[FakeProvider]
  http[OpenAI-compatible HTTP]
  val{Pydantic validation}
  out[JSON on stdout or tool result]
  fail[ExtractionFailedError]

  cli --> ext
  mcp --> ext
  ext --> reg
  ext --> prov
  prov --> fake
  prov --> http
  ext --> val
  val -->|valid| out
  val -->|invalid, attempts left| ext
  val -->|attempts exhausted| fail

A provider returns text. The extractor is the only place that decides whether that text is a successful record. Timeouts and HTTP errors skip the retry loop: there is no new validation error to show the model, and repeating the same request would spend the timeout again.

Design decisions

One method on the provider. complete(messages, schema_name, json_schema, timeout) -> str. The OpenAI client, the offline parser, and the test double (ScriptedProvider) all implement it. Tests never open a socket.

Retries are only for validation. Invalid JSON, extra fields, a string where an integer is required, a money value of "182.5", a salary range that is upside down: those go back to the model as Validation error: plus the Pydantic messages, with the bad reply as the previous assistant turn. The cap is max_attempts (default 3).

Strict models. extra="forbid" and strict=True, so "10" is not accepted as the integer 10. The JSON schema sent upstream also sets additionalProperties: false and marks every property required, which is what OpenAI strict mode asks for.

Money is text. Invoice amounts are strings with two fractional digits. JSON numbers are binary floats. The models do not check that line items sum to the total.

The fake provider is a parser for the files in examples/. Those files are emails with labels (Vendor:, Line items:, and so on). If a label is missing it raises UnrecognizedInputError. It is not used when the OpenAI provider fails.

httpx, not the OpenAI SDK. The request is a small JSON body. Any server that implements POST /chat/completions can sit behind OPENAI_BASE_URL. The API key is stripped from HTTP error text if the server echoes it.

One fence is stripped. If the whole reply is a markdown JSON fence, the fence is removed before validation. Surrounding prose is not scraped for a JSON object. If the inner text is still invalid, the attempt counts as a failure.

Logs. Info logs record the schema, the attempt, and the length of the input. They do not record the input. Validation failures are warnings. The CLI stays quiet unless something fails or you pass --verbose.

Layout

src/structured_extract/
  cli.py            argparse entry point
  config.py         environment variables
  exceptions.py     typed errors
  extractor.py      validation and retry
  mcp_server.py     stdio MCP server
  providers.py      fake, scripted, and HTTP providers
  schemas.py        invoice, job posting, support ticket
examples/           sample emails the fake provider can parse
tests/              pytest, no network
.github/workflows/ci.yml

Public import surface: extract, get_schema, available_schemas, and the exception types.

Development

uv sync --extra dev
uv run ruff check .
uv run pytest

CI runs those two commands on Python 3.11 and 3.12.

Limitations

  • The offline provider only understands the labeled layout in examples/. Other text fails. That is intentional.

  • HTTP 429, HTTP 5xx, connection errors, and timeouts are not retried.

  • There is no check that line-item totals add up to the invoice total.

  • The strict JSON schema helper covers these models. It is not a general converter for every keyword OpenAI strict mode rejects (allOf, numeric constraints on some servers, and so on).

  • temperature is always 0. A server that rejects that field needs a code change.

  • The MCP server is stdio only. There is no authentication and no container image.

  • Nothing here has been measured against a model benchmark. The tests check the retry loop, the HTTP request shape, and the offline parser.

Available Tools

2 tools
extractB

Extract validated JSON for schema from text.

Uses STRUCTURED_EXTRACT_PROVIDER (default: fake). The fake provider only understands the labeled examples in this repository. Failures raise a tool error instead of returning placeholder JSON.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYes
schemaYes

TDQS

B3.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it does disclose real behavior: the provider dependency (STRUCTURED_EXTRACT_PROVIDER, default fake), the fake provider's limitation to repo examples, and that failures raise a tool error rather than returning placeholder JSON. However, it says nothing about the shape of a successful result or any cost/latency/auth characteristics.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Short and front-loaded: the capability sentence comes first, followed by the provider caveat and error behavior. No filler, though the provider detail is somewhat implementation-flavored.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 2-required-param tool with no output schema, the description covers error behavior and provider caveats but omits the successful return shape and the valid values for 'schema'. An agent could call it, but not confidently about the output.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must compensate, and it does assign roles (JSON validated against 'schema', sourced from 'text'). It still leaves ambiguity about whether 'schema' is an inline JSON string or a schema name from list_schemas, and gives no format detail for either parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

It states a specific verb+resource: 'Extract validated JSON for schema from text'. That is clear and distinguishable from the sibling list_schemas, which enumerates rather than extracts, though the description never names the sibling to make the contrast explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no when-to-use or when-not-to-use guidance, and no mention of list_schemas as the way to obtain a valid schema value. Usage is only implied by the parameter pairing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_schemasA

Return the names of the extraction schemas this server can fill.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It does disclose the return shape (names only, not full schema definitions), which is genuinely useful, but says nothing about read-only safety, auth requirements, or whether the list is static. For a zero-argument listing tool this is thin but adequate.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence with zero waste, front-loading the verb and the exact payload returned.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a trivial zero-param read tool with no output schema, the description covers what is returned and implies the discovery role. Only the relationship to 'extract' and the absence of any safety signal keep it from being fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes no parameters, so per the baseline there is nothing for the description to disambiguate. The description correctly signals a no-input call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: it returns the names of the extraction schemas the server can fill. The scope ('names' only) is clear, and it is implicitly distinguishable from the sibling 'extract', which fills a schema rather than enumerating them. It stops short of explicitly naming that sibling or the relationship.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: an agent can infer this is a discovery step preceding 'extract', but the description never says when to call it or that it is a prerequisite for extraction. No exclusions or alternatives are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.1.0
    • First observedextract
    • First observedlist_schemas

TDQS

A3.5/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: list_schemas discovers available schemas and extract performs the extraction. There is no overlap or ambiguity between them.

Naming Consistency4/5

Both names are lowercase snake_case, but one follows verb_noun (list_schemas) while the other is a bare verb (extract). This is a minor deviation within an otherwise consistent convention.

Tool Count3/5

Only 2 tools makes the surface quite thin, though each one earns its place for a focused extraction service. It sits at the borderline of being under-scoped.

Completeness3/5

The core workflow (discover schemas, extract data) is covered, but there is no way to inspect a schema's fields or validate text without extracting, leaving users partly blind to the expected structure.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables agents to deterministically extract structured entities from input payloads and validate or coerce them against JSON schemas, returning a standardized response dossier with execution telemetry. It runs as a zero-dependency MCP server that plugs into Claude Desktop, Cursor, and other MCP clients.
    8
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables agents to ask typed questions about text or JSON and receive structured answers, with tools for single requests, batch processing, model listing, and offline validation.
    11 npm
    MIT