Skip to main content
Glama
yschang1688

salary-mcp-agent

by yschang1688

tw-salary-mcp

tests

Grounding data for the Taiwanese job market, served to language models under explicit constraints — plus a Claude Agent SDK agent that researches through it.

A chatbot asked about Taiwanese pay will answer fluently from stale, non-local training data, and will happily average numbers that must not be averaged. This server exists to be plugged into that chatbot: named read-only tools over data it does not have, every answer ending with a source-and-vintage stamp.

The dataset is Taiwan's MOPS non-managerial salary disclosures for listed companies, 2019–2025 — public company-level aggregates from the TWSE and TPEx open-data endpoints. The same dataset powers a live explorer at salary-db.pages.dev (1,826 companies, seven-year trends, industry box plots — a single self-contained page).

MCP server (stdio)

salary_mcp/ — four read-only tools + a schema resource, Python

MCP server (remote)

salary-mcp-beige.vercel.app/mcp — the same surface over Streamable HTTP, live on the edge

Agent

agent/researcher.py — Claude Agent SDK, multi-step planning over those tools

Tests

63 passing — 29 Python, 34 driving the remote server over the real protocol

In brief

A model that can query a database is easy. A model that can query a database and cannot do anything else takes some design. This repo is mostly about the second problem: what the tool surface should look like when the caller is a language model, and how to prove the constraints hold.

Related MCP server: psx-mcp

Quick start

uv venv && uv pip install -e ".[dev]"
.venv/bin/python -m pytest          # 29 passed

Run the server on its own (it speaks MCP over stdio, so it waits for a client):

.venv/bin/python -m salary_mcp.server

Run the agent (needs ANTHROPIC_API_KEY):

.venv/bin/python -m agent.researcher "崇越科技 (5434) 的薪資水準在同業裡算好嗎?"

The tool surface

Two families, at different evidence tiers — the stamps keep them apart:

Tier

Tool

Answers

census

lookup_company(query)

One company by stock code or name substring

census

industry_stats(industry)

Median, p25/p75, and range across a sector

census

top_by_median(industry, min_median, limit)

Ranked list under filters

census

company_trend(code)

One company's median for each year on record

corpus

analyze_jd(jd)

Which skill groups a job description asks for, with corpus demand share

corpus

skill_gaps(jd, skills)

Covered vs gap groups for the caller's own skills (nothing stored)

corpus

market_demand(query, limit)

Skill-group demand ranking + years-of-experience distribution

census tier = MOPS statutory disclosures: every listed company, statutory filing, no self-selection. corpus tier = deterministic analysis over a versioned 32-group skill dictionary, with demand aggregates from a private, dated corpus of 1,086 Taiwanese data/AI postings (counts only — no posting text, no company names, no attribution by design). The corpus tier never borrows the census tier's authority; a test asserts each family carries its own stamp and not the other's.

Plus a salary://schema resource describing the fields, units, and the two reading rules that matter (below).

Side by side: the same question, bare vs grounded

Q: 台積電和聯發科哪家分紅比較好?我五年經驗大概可以拿多少?

Bare chatbot

Same chatbot + this server

A fluent paragraph quoting round numbers from training data of unknown age, comparing "bonus" figures that mix 分紅, 年終, and total pay — no way to tell which year, which population, or whether the numbers are real.

lookup_company returns each company's statutory median with — Source: MOPS statutory disclosures (TWSE/TPEx), 2019-2025 vintage. Census-grade. The model can compare like with like, say which year it is quoting — and decline the parts of the question the data cannot answer (individual bonus structure is not in a company-level census, and the server says so instead of improvising).

The difference is not eloquence. It is that one answer can be checked and the other cannot. Three properties do the work:

  1. Determinism — same JD, same answer. A versioned dictionary with word-boundary matching (ai never matches maintain), not a generation.

  2. Refusal — populations that must not be mixed stay unmixed. Validation errors come back as tool errors the model can read, not as improvised numbers.

  3. Stamps — every answer ends with its source and vintage. A stale dataset degrades into a labelled snapshot, never a silently wrong answer. This is asserted per tool, including empty-result answers.

What the matcher is, measured

The corpus-tier matcher was scored against an 85-posting golden set, human-adjudicated hit-by-hit in the upstream (private) pipeline. Labels restricted to the 32 public groups:

precision 0.54 · recall 0.89 · F1 0.67

Read the shape, not just the number: this is a deliberately recall-first screening layer — it would rather flag a group for you to reject than miss one. It is not a verdict layer, and no claim is made that it "beats" any model at reading a JD. What it has that a model does not: the same answer twice, a version number, and a measured error profile.

Snapshot semantics. The corpus is a dated snapshot (window and size are in every stamp). There is no freshness promise and no SLA; regeneration is a documented runbook on the private side, and when it runs, the stamps change.

Design decisions

No free-form query tool. There is deliberately no run_query, no SQL passthrough, no eval. Every question the model can ask is a named tool with a typed signature, so the reachable query space is those four functions and nothing else. A generic query tool would hand the model — and anything that can prompt-inject it — the full expressive power of the query language. This is the decision the rest of the design follows from.

Read-only by construction. No write, delete, or exec tool exists. Only dataset.py touches the filesystem, and only inside data/.

Arguments are validated before use, and clamped server-side. Stock codes must match ^\d{4,6}$; free text is length-capped; limit is clamped to 50 in the server rather than trusted to the caller. Path traversal and injection-shaped strings come back as a short validation message — never a traceback, never a filesystem path.

Two independent gates. The agent checks each call against an allow-list before it runs, and the server validates arguments again on arrival. A mistake in either one alone is not sufficient to reach the data.

Explicit ceilings. max_turns and max_budget_usd are set rather than left to default. An agent that chooses its own next step needs a limit that does not depend on it choosing to stop.

Every tool call is audited. ToolAudit records what the agent reached for, allowed and denied. A transcript shows what an agent said; only the audit shows what it tried.

Two rules the data forces on the answer

These are in the system prompt because getting them wrong produces confident, wrong numbers:

  • Quote the median, not the mean. When median/mean falls below about 0.85 the distribution is right-skewed — the mean is being pulled up by a few high earners. The server reports the ratio and flags it.

  • A missing year is missing, not zero. A company absent from a year was below the disclosure threshold or not yet listed. company_trend returns null for those years and names them; rendering them as zero would invent a pay cut that never happened. 台灣虎航 (6757) is the worked example — listed part-way through the window, and there is a test asserting its gap years never render as 0.0.

Verification status

Being precise about what is and isn't covered:

Status

MCP protocol — handshake, tool discovery, tool calls, resource reads

✅ 16 tests against a real server subprocess over stdio

Argument validation, clamping, traversal and injection rejection

✅ Covered, including the paths that must fail

Security posture (no mutating or free-form tool is exposed)

✅ Asserted, and the assertion is self-validated (below)

Agent permission gate, allow-list, audit, ceilings

✅ Unit-tested, both outcomes

Agent allow-list matches the tools the server actually serves

✅ Verified by launching the real server from the agent's own config

The agent loop itself — SDK driving a model through the tools

Not covered. Requires API credentials, which are not present in the environment this was built in. The code follows the documented SDK API and constructs against the real SDK types, but no live multi-turn run has been executed.

The posture assertions are self-validated. Guard tests that only ever pass are the ones you should trust least, so the check was verified against a known-bad input: injecting a run_query tool into the server makes three tests fail (test_no_mutating_tool_is_exposed, test_handshake_and_tool_discovery, test_every_allow_listed_tool_exists_on_the_real_server). Removing it returns them to green. The first attempt at that probe appended the tool after mcp.run(), where it never executed — so the tests passed and briefly looked broken. Worth stating because the failure mode is general: a guard test verified with a probe that isn't actually bad tells you nothing.

Data

data/salary-{2019..2025}.json — snapshots of the TWSE and TPEx t187ap46 open-data endpoints. Public, company-level aggregates; no personal data. Figures are 萬元 (NT$10,000) per year.

Two upstream quirks the loader handles: six 2025 rows carry a null industry, and companies enter and leave the dataset as they cross the disclosure threshold.

Layout

salary_mcp/dataset.py   loading, validation, queries — the only filesystem access
salary_mcp/server.py    MCP tools and the schema resource (stdio)
agent/researcher.py     Agent SDK options, permission gate, tool audit
tests/                  16 protocol tests, 13 agent-guardrail tests
web/src/lib/            the same query layer and tool surface, in TypeScript
web/src/app/mcp/        JSON-RPC 2.0 endpoint — Streamable HTTP on the edge
web/test/               18 protocol and boundary tests against a live server

Remote MCP server (web/) — live

https://salary-mcp-beige.vercel.app/mcp

The stdio server has to be cloned and run before anyone can use it. web/ is the same tool surface as a remote MCP server: add that URL to any MCP client and the four tools are there, no install, no key.

curl -s -X POST https://salary-mcp-beige.vercel.app/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"lookup_company","arguments":{"query":"2330"}}}'
cd web && npm install && npm run build
npx next start -p 3466            # then: node --test test/protocol.test.mjs

The suite also runs against the deployment — the same 18 tests, no local server:

MCP_URL=https://salary-mcp-beige.vercel.app/mcp node --test test/protocol.test.mjs

It runs on the Vercel Edge Runtime (a V8 isolate, not Node), which drove two design choices worth naming:

  • The JSON-RPC layer is hand-rolled. @modelcontextprotocol/sdk's StreamableHTTPServerTransport is built on Node's http request and response objects, which do not exist in an isolate. Streamable HTTP is JSON-RPC 2.0 over POST, so the surface a stateless read-only server needs is small enough to write directly against the Web-standard Request/Response — which is what makes it deployable to the edge at all.

  • It is stateless: no sessions, no SSE stream. GET returns 405 with a pointer to POST. The spec permits a server to decline the server-initiated stream, and for pure reads there is nothing to push.

The dataset is trimmed at build time to the fields the tools actually use (393 KB for seven years), so it is bundled rather than fetched — an edge function has a 1 MB code-size limit on the free tier.

Errors are split deliberately: a bad argument comes back as a tool error (isError) so the model can read the message and retry, while a bad request comes back as a JSON-RPC error. Neither path returns a stack trace or a path.

The guard test is self-checking. GUARD: no generic query tool is exposed asserts the property the whole design rests on. Planting a tool named query in web/src/lib/tools.ts turns three tests red; removing it returns 18/18 — verified, not assumed.

Deployment check (2026-08-14): 18/18 against production, and x-vercel-id: hkg1::sin1 confirms it is served from the edge network rather than a single origin — the claim is measured, not inferred from the config.

Licence

MIT.

Available Tools

4 tools
company_trendA

Show one company's median pay for every year on record (2019-2025).

Args: code: A 4-6 digit TWSE/TPEx stock code, e.g. "2330".

ParametersJSON Schema
NameRequiredDescriptionDefault
codeYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/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 burden of behavioral disclosure. It does disclose the year range (2019-2025) and the stock code format, which adds context. However, it does not describe the output structure, error behavior, or any access/rate limits. Since it's a simple read-only query tool, the description provides adequate but minimal behavioral context.

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?

The description is concise and front-loaded: the main purpose is in the first sentence, followed by a clearly formatted Args section. Every sentence provides necessary information without redundancy. It is appropriately sized for a simple single-parameter tool.

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?

The tool has one parameter, no annotations, and an output schema. The description covers the purpose and the parameter format, which is sufficient for the tool's simplicity. The presence of an output schema means return value details are documented elsewhere. A minor gap is the lack of explicit guidance on what the output looks like (e.g., a table or list), but it is not necessary. Overall, the description is nearly complete for this context.

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

Parameters5/5

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

The schema description coverage is 0%, and the schema only lists 'code' as a string with no meaning. The description compensates fully by explaining that the parameter is a 4-6 digit TWSE/TPEx stock code and provides an example ('2330'). This is essential and adds complete semantic value beyond the schema.

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

Purpose5/5

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

The description clearly states the tool shows one company's median pay for each year in a specific range (2019-2025). This uses a specific verb ('show') and a resource ('one company's median pay'), and the year range adds specificity. The sibling tools (lookup_company, industry_stats, top_by_median) appear to serve different purposes, so this description effectively distinguishes the tool.

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

Usage Guidelines4/5

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

The description implies the use case: to retrieve annual median pay data for a single company. It provides a clear context for when the tool is appropriate but does not explicitly contrast it with sibling tools or state exclusions. Since it gives a focused purpose, it is above the 'implied usage' level but lacks explicit alternatives.

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

industry_statsA

Summarise the pay distribution within one industry.

Use this to judge whether a single company's pay is high or low for its sector — an absolute figure means little without the sector baseline.

Args: industry: Industry name or part of one, e.g. "半導體", "航運業", "電子通路".

ParametersJSON Schema
NameRequiredDescriptionDefault
industryYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/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 states the tool summarizes pay distribution and explains its intended purpose, but it does not disclose specific output details or side effects. For a read-only summarization tool, this is adequate but not rich.

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?

The description is compact and well-structured: a purpose statement, a usage rationale, and a parameter list with examples. Every sentence adds value and there is no redundancy.

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

Completeness5/5

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

Given the tool's simplicity (one parameter, output schema exists), the description fully covers purpose, usage, and parameter semantics. The output schema handles return details, so the description is complete for an agent to select and invoke the tool correctly.

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 schema only defines 'industry' as a required string with no description. The description adds valuable guidance: 'Industry name or part of one' and provides concrete examples, clarifying that partial matches are acceptable.

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

Purpose5/5

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

The first sentence 'Summarise the pay distribution within one industry' uses a specific verb and resource, clearly defining the tool's function. The second sentence adds the sector-baseline use case, which distinguishes it from sibling tools like lookup_company or top_by_median.

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

Usage Guidelines4/5

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

The description explicitly says to use this tool to judge whether a single company's pay is high or low for its sector, providing a clear when-to-use scenario. It does not explicitly name alternatives or exclusions, but the context is sufficient for most agents.

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

lookup_companyA

Look up one company's salary disclosure by stock code or name.

Args: query: A 4-6 digit TWSE/TPEx stock code (e.g. "2330"), or part of a company name (e.g. "台積"). Names are matched as a substring, so a short query can return several companies.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior4/5

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

Without annotations, the description carries the behavioral disclosure burden. It explicitly discloses the substring matching behavior and the possibility of multiple results, which is key for managing expectations. It does not address error handling or return format, but an output schema exists to cover that.

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?

The description is two sentences plus an argument explanation, front-loaded with a clear purpose. Every sentence provides useful information without redundancy.

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

Completeness5/5

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

Given the single-parameter simplicity and presence of an output schema, the description covers the essential context: what the tool does and how to formulate queries. The substring caveat prevents misuse. No significant gaps remain.

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

Parameters5/5

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

The schema provides no description for the 'query' parameter (0% coverage), but the description fully compensates by explaining acceptable formats (4-6 digit code or name substring) with examples. This exceeds the baseline and is highly actionable.

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

Purpose5/5

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

The description clearly states the tool's function: 'Look up one company's salary disclosure by stock code or name.' It includes specific input formats and even notes substring matching, distinguishing it from sibling tools that likely provide aggregate or trend data.

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

Usage Guidelines4/5

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

The description implies usage for single-company lookups, with clear input guidance (stock code or name). It does not explicitly mention alternative tools, but the scope is evident. The caveat about short queries returning multiple companies provides additional context.

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

top_by_medianA

Rank companies by non-managerial median pay, highest first.

Args: industry: Optional industry filter (substring). Empty means all industries. min_median: Optional floor in 萬元/yr, e.g. 120 keeps only companies paying a median of NT$1.2M or more. limit: How many to return (1-50; values above 50 are clamped to 50).

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
industryNo
min_medianNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations, the description carries the behavioral disclosure burden. It details sort order (highest first), substring filtering for industry, the floor logic for min_median with an example, and the clamping behavior for limit. This goes beyond the schema, though it does not cover error behavior or data source nuances.

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?

The description is extremely concise: one purpose sentence followed by a clean Args list. Every sentence earns its place, and the structure is easy to scan; no redundancy or filler.

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

Completeness5/5

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

Given the tool has only three optional parameters and an output schema exists, the description is nearly complete. It covers the ranking metric, ordering, filtering options, limiting behavior, and unit interpretation. The example clarifies the min_median semantics, making the tool well-specified for an AI agent.

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

Parameters5/5

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

Schema description coverage is 0%, and the description fully compensates by explaining each parameter: industry as an optional substring filter, min_median as a floor in 萬元/yr with a concrete example, and limit with range and clamping. This adds substantial meaning beyond the bare schema.

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

Purpose5/5

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

The description clearly states the tool ranks companies by non-managerial median pay, highest first. This specific verb+resource (rank companies) and the metric (median pay) distinguishes it from sibling tools like lookup_company and company_trend.

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?

The description implies usage (ranking top companies) but does not explicitly state when to use this tool versus alternatives such as industry_stats or company_trend. It provides no when-not-to-use guidance or mention of alternative tools.

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. Dates show when Glama detected each change.

  1. 4 tool updatesv0.1.0
    • First observedcompany_trend
    • First observedindustry_stats
    • First observedlookup_company
    • First observedtop_by_median

TDQS

A4.5/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct aspect of salary data: specific company lookup, industry aggregation, company ranking, and company time trend. No overlapping purposes; an agent can easily choose the right tool.

Naming Consistency5/5

All tool names follow a consistent snake_case pattern with a clear verb or noun style: lookup_company, industry_stats, top_by_median, company_trend. The naming is predictable and readable.

Tool Count5/5

Four tools is a well-scoped set for a salary data server. Each tool fills a necessary role without redundancy, and the count is within the ideal range.

Completeness4/5

The core workflows of querying a company, benchmarking against an industry, ranking, and viewing historical trends are covered. Minor gaps exist, such as direct company-to-company comparison, but the tool surface is largely complete for the stated domain.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    US + EU salary benchmarking, pay transparency compliance, and semantic endpoints. 1,400+ US occupations, 28 EU countries. MCP server for AI agents.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server that exposes Pakistan Stock Exchange data (quotes, dividends, announcements, indices) as LLM-callable tools, enabling conversational market queries in plain English.
    4
    9
    4
    MIT
  • F
    license
    A
    quality
    B
    maintenance
    This MCP server lets AI assistants search job listings on Taiwan's 104 Job Bank, read full job details, and prepare applications with human-in-the-loop confirmation before final submission.
    3
    2
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yschang1688/tw-salary-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server