
Typecast
io.github.nilayp-codes由 GitHub 账号 nilayp-codes 发布v0.1.0更新于 Nov 7, 2026
Validates job-posting JSON against a strict schema; returns one fixable error line per problem.
概览
一个本地小型 MCP 服务器,按严格 schema 校验职位发布 JSON,并为每个问题返回一行可修复的错误信息。
- 可以做什么
- 获取职位发布的 JSON Schema,让助手了解期望的字段与类型
- 校验以单个字符串传入的完整职位发布 JSON,返回是否有效、错误行列表,以及通过时的解析结果
- 检查 schema 无法表达的规则,例如重复的技能名称和实习/经验规则,这些只有校验器会检查
- 适用场景
- 当助手生成或修改结构化职位发布数据,并希望在保存或传递前按固定 schema 逐条检查、且错误信息便于模型自行修正时使用。
- 运行要求
- 作为 stdio 本地进程运行,从 PyPI 安装 typecast-mcp,通常用 uvx 启动(需要 uv),也可用 pip install。无需 API 密钥、环境变量或网络访问。
安装
在 SourceWeft 中
- 打开 控制台中的 Typecast,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
Typecast
LLM agent that turns messy job descriptions into schema-validated, typed JSON — and measures how often that fails.
Key findings (two Qwen models via OpenRouter, 30 real job descriptions):
- Schema mode makes output valid, not correct. In setup C both models were valid 98.9% of the time, at lower cost than retry, yet end-to-end accuracy was 67.1% (7B) and 84.7% (235B).
- Error-feedback retry fixed the small model, not the large one. Setup B was 98.9% valid for the 7B but 88.9% for the 235B, which failed on jd_002 in all four setup-B runs. A count-aware retry message did not change that.
- No run reached my 90% field-accuracy goal (best: 87.3%), and runs vary. The same setup moved by about 8 points end to end between runs, so a gap between two single runs is not conclusive.
60-second walkthrough:
https://github.com/user-attachments/assets/c4a2572c-88a2-49a3-8e20-176010877154
CLI and package name: soa · MCP server on PyPI: typecast-mcp
Results
Two open-weight models through OpenRouter, 30 real job descriptions, four setups. Field accuracy is scored against 20 hand-verified gold labels (see Limitations for how they were made). With 3 runs, the figure is the mean and the brackets are min–max across the runs; with 1 run it is that single run. A = plain prompt, no retry; B = plain prompt + error-feedback retry; C = provider schema mode, no retry; D = schema mode + retry.
Full table: first-try valid, avg calls and field accuracy too
How to read it:
- "Mean of 3 runs (min–max)" (B and C) is the mean of three runs on the same 30 documents, with the lowest and highest run in brackets. It is a spread across runs, not an interval estimate. The B runs are the three runs made after the retry-feedback fix described below. The C runs are the first C run plus two repeats. "Single run" (A and D) is one draw each: the repeats show the same setup moving by about 8 points end to end between runs (235B setup B: 72.8% to 80.9%). Each run's own 95% interval (Wilson for the validity columns, bootstrap for the accuracy columns) is in the per-run table at the end of this section; the single runs of A and D have none, because they were made before the interval code existed.
- First-try / final valid: share of the 30 documents whose first reply, or any reply within 3 attempts, passed validation. Avg calls: model calls per document.
- Field accuracy (valid outputs) is measured only on documents that produced a valid posting, so it flatters a setup that fails often (235B setup A: 86.6% field accuracy, but only 70.0% of the labelled documents had a valid output to score). End-to-end accuracy counts a failed document as wrong on every field and is the number to trust when setups differ in validity.
- Cost is the provider-reported cost of one run over the 30 documents. Temperature was not set (the provider default applied, value unknown).
My targets, and whether they were met
I set three targets for setup B before running anything: final validity of 30/30, at most 1.3 calls per document on average, and at least 90% field accuracy. Result, per run (the three runs after the retry fix):
The 90% field-accuracy target was missed by every run of both models, in every setup. The best number anywhere is 87.3% (235B, setup B, first-round run).
Every run, with 95% intervals (results/summary.md, verbatim)
Rows 1-8: the first round, one run per model and setup (A, B, C, D for the 7B, then for the 235B). These were written
before the interval code existed, so they have none. Rows 9-18: the repeat round, in order 7B B x3, 7B C x2, 235B B x3,
235B C x2. Intervals are in brackets. Columns: field acc is over valid outputs, coverage is the share of the 20
labelled documents with a valid output, e2e acc counts failures as wrong.
The A and D figures in the main table are rows 1-8 (A and D) above. The first-round B rows (7B: 86.7% / 96.7%,
235B: 83.3% / 86.7% first-try / final) are the runs from before the retry fix; the first-round C rows are the first
of the three C runs in the main table. The aggregate over the repeats is in results/aggregate.md; the raw per-run
reports are results/2026-11-04_*.json.
How it works
The schema shown to the model is generated from the Pydantic model (JobPosting.model_json_schema()), never written
by hand, and the reply is checked with the same model. A failed reply goes back to the model with the validator's
errors, one line per error, for up to 3 attempts. After a valid reply, two deterministic tools tidy the result
(canonical skill names, a location check against the document header). They are plain code, not model calls, so they
add no LLM calls and the setups below stay comparable. Details: docs/ARCHITECTURE.md.
The four setups: A plain prompt, no retry. B plain prompt, errors fed back (up to 3 attempts). C the provider's strict JSON-schema mode, no retry. D schema mode plus retry.
Why not just use native structured outputs?
Schema mode helped a lot, and it is cheaper. It also does not do the whole job.
What it fixed. Setup C was valid on the first try 98.9% (96.7%-100.0%) of the time for both models, against a
single plain-prompt run (A) of 83.3% for the 7B and 70.0% for the 235B. The 30-item cap on skills was the constraint
that models actually broke in plain mode; under schema mode the report counts replies that broke a constraint the
schema carries, and it was 0 in every C and D run (6 C runs, 2 D runs). C also cost less than B: $0.0076 against
$0.0103 for the 7B, $0.0136 against $0.0211 for the 235B (means per run).
What it does not do:
- It guarantees shape, not meaning. The remaining failures in C were rules no JSON schema can express. Two of the six C runs had one document rejected for duplicate skill names ("LangChain" and "Langchain"; "Python" twice; both rejections are in results/failure_examples.jsonl), which is why C's final validity is 98.9% (96.7%-100.0%) and not 100%. The intern-implies-no-experience rule has the same property (it cannot be stated in a schema); it did not fire in any of the 637 logged attempt lines of this project (the last line of that file records the count), so I have no evidence either way on it.
- Valid is not correct. Schema mode made the 235B valid 98.9% (96.7%-100.0%) of the time, and its field accuracy is 86.1% (85.6%-87.0%), with end-to-end accuracy 84.7% (82.8%-85.8%): below my 90% goal. The 7B in C is just as valid and gets 68.2% (66.5%-69.8%) field accuracy; its seniority accuracy was 55.0%, 55.0% and 52.6% over the three C runs. Validity says nothing about whether the seniority is right.
- It may trade recall for validity. The cap is enforced by the model emitting shorter skill lists, and skills are where accuracy is weakest. For the 235B, skills F1 was 66.9%, 72.1% and 65.7% in the three C runs, against 71.4%, 71.6% and 68.3% in the three B runs (means 68.2% vs 70.4%, with overlapping ranges). In the first-round single runs it was 72.1% (A) and 73.8% (B) against 66.9% (C) and 63.4% (D). The repeats show this is a tendency, not a clean effect, and with n=20 labelled documents I would not claim more.
- It depends on the provider. Both models accepted strict
json_schemahere, but only because requests were sent with OpenRouter'srequire_parametersso they could only route to providers that support it. The 7B was always served by one provider; the 235B was split across three.
Most common failure types, and what fixed them
From the plain-prompt runs and the retry runs (counts are failing attempts whose first error was at that field,
failures.by_first_loc in the reports):
Almost everything that goes wrong is a count cap, not a formatting problem. In the first 11-document baseline every
failure of both models was a too-long skills list (results/2026-11-03_baseline_*.md). The first thing I tried was
to make the feedback more specific. The default feedback line is the validator's own wording (for example skills: List should have at most 30 items after validation, not 42). I replaced it for over-long lists with a count and a
fix, for example skills: has 39 items, maximum is 30 - remove 9, starting with required=false ones. The result is
mixed:
- It did not lift the 235B's final validity in setup B: 86.7% before, then 86.7%, 93.3% and 86.7% across three runs. Each run's interval contains the old figure, so at n=30 no effect is shown.
- The four 235B documents that failed in the first B run (jd_002, jd_003, jd_005, jd_023) passed in 6 of 12 document-runs afterwards (jd_002 0 of 3, jd_003 1 of 3, jd_005 2 of 3, jd_023 3 of 3), but other documents (jd_029, jd_030, jd_004) failed instead. I cannot separate the fix from run-to-run variation. The first attempt is identical before and after the fix, yet first-try validity moved from 83.3% to 76.7%, 73.3% and 80.0%; that spread is run-to-run variation (sampling plus a provider mix I did not log in the first round).
- jd_002 failed for the 235B in all four setup-B runs. Every attempt is in
results/failure_examples.jsonl: 42, 31, 31 skills in the first run (before the
fix), then 70, 31, 31 and 47, 31, 31 and 44, 31, 31 in the three runs after it. The model goes from 42-70 skills down
to 31 on the first retry and then stays at 31. For a 31-item reply the new feedback line is
skills: has 31 items, maximum is 30 - remove 1, starting with required=false ones(built by the code; the prompt text itself is not logged). This is the clearest open failure: the model does not make the last cut.
Design decisions
strict=Trueon every model. Pydantic's default lax mode quietly turns"3"into3and"yes"intoTrue, so a sloppy reply passed and the retry loop had nothing to do. Strict mode rejects both.- Required-but-nullable fields (
company,city,country,min_years_experience). No default: the model has to writenullexplicitly, so "not stated" is a decision the model made, and a forgotten key is an error rather than a silentnull. - One line per error in the retry prompt,
loc: msg (got: <input, clipped to 60 chars>), rendered fromValidationError.errors()instead ofstr(error). The raw string carries a version-specific prefix, a truncatedinput_value, and a documentation URL, which are noise for a model. The one exception is an over-long list, which gets the count-aware line shown above. I did not run a blind-retry control (retry with no error text), so I have no measurement of feedback versus plain resampling; setup A against B is retry-with-feedback against nothing. - Tools are deterministic post-processing, not model-called. Canonical skill names and a location check run
after validation as plain code. That keeps every setup at one prompt and the retry/attempt/cost counts clean, and
the model's own output is kept (
raw_posting) so validity is measured on what the model actually said. Letting the model call the tools is a separate experiment I have not run. - API errors are separate from validation errors. A 429 or 5xx is retried with backoff (2, 4, 8 s) and never uses up one of the 3 validation attempts.
- The 30-skill cap stays. The first plain-prompt baseline showed every failure was this one constraint, so I kept it as the test of what retries and schema modes can do rather than loosening it to make numbers look better.
Limitations
- 30 job descriptions, 20 gold labels. One document moves a validity rate by 3.3 points and a field accuracy by 5 points; the intervals above are wide for that reason (for example 235B setup B final validity 93.3% [78.7-98.2] in one run). Five of the 30 are non-technical or semi-technical roles, kept on purpose as an out-of-domain slice; two of them (jd_012, jd_019) are among the 20 labelled.
- The gold labels were drafted by a model (Claude) and then checked by me. I verified every field and made 17 corrections (docs/label_corrections.log). One person, one pass, so any systematic mistake of mine is in the labels. The labelling rules were written into the schema's field descriptions after the first baseline, so the models and the labels follow the same rules; runs on the earlier prompt are not comparable.
- Noise floor on accuracy, from a blind check. On 5 other documents (jd_004, 005, 008, 014, 015, none of them in the gold set) I labelled blind and compared against Claude's sealed labels written beforehand, with the same per-field metrics as the eval (my labels as the reference): title 80.0%, company 100.0%, seniority 80.0%, mode 80.0%, salary_mentioned 100.0%, min_years_experience 100.0%, city 80.0%, country 100.0%, skills F1 61.0% (precision 52.9%, recall 72.0%), overall 86.8% (results/label_agreement.md). Five documents is a sanity check, not a rate: one disagreement moves a field by 20 points. It does suggest that on skills two careful labelers disagree about as much as the models disagree with the gold labels, and that the 235B's roughly 86% field accuracy is close to the level two labelers reach with each other, so the remaining gap to my 90% target may partly be label noise rather than model error. I have not shown that.
- Runs are from one day, two models of one family, one provider layer. Temperature was not set. The 7B was always routed to one provider; the 235B to three, and the provider was not logged in the first round, so provider mix and sampling noise are mixed into the run-to-run spread.
- The post-processing tools were not tuned against the final labelling rules, so the location check and the labels can disagree on some documents.
- Setup D is a single run per model; only B and C were repeated.
- Data. The 30 job descriptions in
data/jds/were copied from public postings (LinkedIn 13, Work at a Startup 11, Wellfound 6, counted fromdata/jds/manifest.csv) for evaluation only; I will remove any of them on request. Five rows have no source URL recorded inmanifest.csv: jd_001, jd_002, jd_003, jd_004 and jd_010 (all LinkedIn; their notes say "URL not recorded").
Run it yourself
Needs Python 3.11+ and uv.
Known issue: on Windows, clone into a short path, because LiteLLM writes a cache file under .venv and a very deep path exceeds Windows' path-length limit (soa extract then fails with APIConnectionError ... [Errno 2]).
soa eval makes real model calls: it prints the plan and the estimated number of calls and asks before sending
anything (--yes skips the question). In my runs one setup over the 30 documents cost between $0.0076 and $0.0234.
It saves results/<date>_<model>_<setup>.json and appends a row to results/summary.md; --resume <eval id>
continues an interrupted run. To combine repeated runs: uv run python scripts/aggregate_runs.py results/<files>.json
(writes results/aggregate.md). The 30 job descriptions are in data/jds/ and the gold labels in data/labels/.
Tests and type checks (no real model is ever called in tests):
Use it from an AI agent (MCP server)
typecast-mcp is a small MCP server that checks a job-posting JSON against the
JobPosting schema and returns one-line errors an agent can act on. It has no LLM, needs no API key and makes no
network calls. It is its own package (packages/typecast-mcp/) with two dependencies,
pydantic and mcp, so installing it does not pull in LiteLLM or the rest of the eval harness.
Two tools:
get_job_posting_schema: the JSON Schema of a job posting. It does not carry every rule (duplicate skill names and the intern/experience rule are only checked by the validator).validate_job_posting: takes one argument,posting, the whole job-posting JSON as ONE string. Returnsvalid, a list of error lines (field: problem (got: value)), and the parsed posting when valid.
Install (needs uv). Claude Desktop, Cursor and other clients:
Claude Code: claude mcp add typecast -- uvx typecast-mcp. On Windows, if the client cannot find uvx, put the full
path to uvx.exe in command (find it with where.exe uvx). The Microsoft Store build of Claude Desktop does not
read %APPDATA%\Claude\claude_desktop_config.json; open Settings → Developer → Edit Config to open the file it
does read. Without uv: pip install typecast-mcp and use typecast-mcp as the command.
From a clone. The repo ships a .mcp.json that runs the server from the working tree (uv run typecast-mcp).
Open the folder in Claude Code, approve typecast when asked, and check it with /mcp. Approving runs uv run in
this folder, which installs the locked dependencies and starts the server. Only approve it in a clone you trust.
A real example. Qwen3-235B's reply on jd_002 had 70 skills. An agent checked it with validate_job_posting and got
one line back:
The agent fixed the posting in one round. This was one run with Claude Sonnet 5.5, not a benchmark.
Demo in Claude Desktop (errors planted for the demo). Claude calls validate_job_posting, gets two error lines,
fixes them and re-validates. Note that it had to guess the years to make the posting pass:
https://github.com/user-attachments/assets/4190ef12-fc7c-46f8-ad48-dd128d80aa54
What it does not check: whether the values are correct. A posting can be valid and still have the wrong seniority or the wrong skills; see Why not just use native structured outputs?.
Details, limits and design decisions: docs/MCP.md.
What I'd do next
A validator-only MCP server: done, see Use it from an AI agent.Publish on PyPI: done for the MCP server,typecast-mcp.- Fix the remaining failure the data points at: the model that cannot make the last cut on jd_002, for example by telling it which items to drop. Re-measure with temperature fixed and the provider pinned, so repeats differ less.
- A second labeler for the gold set, a bigger document set, and repeats of setup D, so the field-accuracy numbers stop being limited by n=20.
License
MIT. See LICENSE.
来源:README.md,提交 0f686aa
工具
0版本历史
1- v0.1.0最新Nov 7, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分