Skip to main content
Glama

pingen-mcp

Send physical letters — A-Post, B-Post, registered — straight from a PDF via the Pingen v2 API.

npm   CI   node   licence


MCP server for Pingen v2 — send real, physical letters (A-Post / B-Post / registered / Einschreiben) from a PDF and track them, straight from any MCP client (Claude Code, Claude Desktop, …). It talks to the official Pingen REST API — no browser automation. Pingen prints and mails the letter for you; data is hosted in Switzerland.

Credentials are never stored in this repo. They are read at runtime from the macOS login keychain (or from environment variables). Nothing secret is committed.


Prerequisites

  • Node.js ≥ 20.19 (node --version)

  • A Pingen account with an OAuth client (client_credentials grant) — see below

  • macOS (the credential lookup uses the security keychain tool; on other platforms use the environment-variable alternative instead)

git clone https://github.com/sapn95/pingen-mcp.git
cd pingen-mcp
npm install

Related MCP server: MCP Email Service

Credential setup (step by step)

This is the part that must be right, or nothing works. You need three values from Pingen and you store them in the macOS keychain.

1. Create a Pingen account

Sign up at https://app.pingen.com. A free account is enough to create drafts and test; you only pay when you actually mail a letter.

2. Create an OAuth client (to get Client-ID + Client-Secret)

  1. In the Pingen dashboard open Settings → API / Developer (also reachable at https://app.pingen.com/organisationAPI).

  2. Create a new OAuth client / application.

  3. Choose the client_credentials grant (machine-to-machine; no redirect URL, no user login at request time).

  4. Copy the Client-ID and the Client-Secret. The secret is shown only once — copy it now.

3. Find your Organisation UUID

Each Pingen organisation has a UUID. It appears in the dashboard URL when the organisation is selected (https://app.pingen.com/.../organisations/<UUID>/...), or under Settings → Organisation. It is optional only when your account has exactly one organisation: the server then calls GET /organisations and uses it. With several, it refuses and lists them rather than deciding — silently — which account pays for and franks the letter.

4. Store all three in the macOS keychain

Run these exactly (the service names must match what index.js reads). Replace the angle-bracket placeholders with your real values:

-w without a value prompts instead of taking the secret from the command line, so it never reaches your shell history or a process listing — where a credential that can print and post mail at your expense has no business being.

security add-generic-password -a pingen -s pingen-mcp-client-id     -w -U   # prompts
security add-generic-password -a pingen -s pingen-mcp-client-secret -w -U   # prompts
# Optional — auto-detected from /organisations if you skip it, and only when
# the account has exactly one:
security add-generic-password -a pingen -s pingen-mcp-org-uuid      -w -U   # prompts

The exact service names read by index.js are:

Value

Keychain service name

Env-var alternative

Client-ID

pingen-mcp-client-id

PINGEN_CLIENT_ID

Client-Secret

pingen-mcp-client-secret

PINGEN_CLIENT_SECRET

Organisation UUID (optional)

pingen-mcp-org-uuid

PINGEN_ORG_UUID

(PINGEN_API_BASE overrides the API base URL, default https://api.pingen.com.)

The organisation UUID is optional only where there is nothing to choose: it is discovered automatically when the account has exactly one organisation, and asked for otherwise. "Exactly one" means one in the account, not one on the page that came back — an organisation list long enough to paginate is a question too, because picking the first entry off it would decide, silently, which account pays for and franks the letter.

An environment variable that is set always wins — even when it is empty. A blank PINGEN_CLIENT_SECRET means no secret, not go and look in the keychain; a blank PINGEN_API_BASE means no endpoint, not use production. Only an entirely absent variable falls through to the keychain, so blanking one is a reliable way to make sure a run cannot reach your real account.

Verify a value is stored (prints the value to your terminal — run only when you're OK seeing it):

security find-generic-password -a pingen -s pingen-mcp-client-id -w

The first time the server reads the keychain, macOS may pop up "node wants to use your confidential information". Click Always Allow so it doesn't prompt on every start.

Never commit credentials. The .gitignore already excludes .env files; the keychain path keeps secrets out of the filesystem entirely.


Register in Claude Code

claude mcp add pingen --scope user -- node /absolute/path/to/pingen-mcp/index.js

That writes an entry into ~/.claude.json. Equivalent manual snippet:

{
  "mcpServers": {
    "pingen": {
      "command": "node",
      "args": ["/absolute/path/to/pingen-mcp/index.js"]
    }
  }
}

Then, in a Claude Code session, run pingen_status — it should print your organisation. That confirms the credentials and registration are correct.

For Claude Desktop, add the same mcpServers block to ~/Library/Application Support/Claude/claude_desktop_config.json.


Draft-vs-send safety model (read this)

Sending a letter is two explicit steps, so you never mail something by accident:

  1. pingen_send_letter uploads the PDF and creates a DRAFT (auto_send = false). Nothing is mailed. You can review it in the Pingen dashboard.

  2. pingen_submit_letter takes an existing draft and physically mails it. This is the step that costs money and puts paper in the post, so it needs confirm: true. pingen_delete_letter needs the same, because a deleted draft does not come back. pingen_cancel_letter does not: stopping a letter is the safe direction.

flowchart TD
    PDF["your PDF"] --> SEND["pingen_send_letter"]
    SEND -->|"default"| DRAFT["draft — nothing is mailed,<br/>review it in the dashboard"]
    SEND -->|"auto_send: true<br/>(needs delivery_product)"| ST
    DRAFT --> ST{"what Pingen says<br/>about the letter"}

    ST -->|"valid — a draft it will take"| SUB["pingen_submit_letter<br/>confirm: true"]
    ST -->|"processing, sent"| POST
    ST -->|"validating"| WAIT["still being checked —<br/>ask again in a moment"]
    ST -->|"action_required, invalid"| STOP["Pingen will not take it.<br/>Sending it again does not help;<br/>a corrected PDF does"]
    WAIT -.-> ST
    SUB --> POST[("mailed — paper in the post,<br/>and it costs money")]

    SUB -.->|"no answer at all: timeout,<br/>dropped line, 5xx"| MAYBE["it says it cannot know.<br/>The letter may already be printing,<br/>so check before sending a second one"]
    SEND -.->|"same, on the auto_send half"| MAYBE

    STOP -.->|"the letter carries the status,<br/>the trail carries the reason"| EV["pingen_letter_events"]
    MAYBE -.-> EV
    DRAFT -.->|"confirm: true —<br/>a deleted draft does not come back"| DEL["pingen_delete_letter"]
    POST -.->|"no confirm needed —<br/>stopping is the safe direction"| CAN["pingen_cancel_letter"]

    classDef gate fill:#fff4e5,stroke:#d9822b
    classDef bad fill:#fdecea,stroke:#c0392b
    classDef done fill:#eafaf1,stroke:#27ae60
    class SUB,DEL gate
    class STOP,MAYBE bad
    class POST done

The only shortcut is passing auto_send: true to pingen_send_letter, which mails immediately without a review step — use deliberately. It also requires delivery_product: the product is optional on a draft only because pingen_submit_letter asks for one later, and auto_send: true is the single route that never reaches that call.

A draft must reach status valid before it can be submitted. If Pingen still needs something (e.g. the address couldn't be read), the draft is action_required and submit will fail — see Troubleshooting. pingen_send_letter says so on the spot: a letter that comes back action_required or invalid is reported as a draft Pingen will not take, with the status and where to look, and not with the usual "now submit it" — on both of its halves, the draft one and auto_send: true, because a refused PDF is refused either way and the half that was told to mail it is the half most likely to be told to try again. pingen_submit_letter says the same thing about the same two statuses rather than "try again": a letter Pingen has refused does not become sendable by being sent a second time, and the note names the one step that helps — a corrected PDF.

All three of those notes point at pingen_letter_events for the reason, because that is where Pingen keeps it: the letter itself carries the status and nothing about why, while the trail carries a bare code (layout_unsupported_format and the like). pingen_get_letter cannot answer that question — it returns the letter row — so being sent there is being told to look somewhere the answer has never been.

All of that reads an answer. The two calls that put paper in the post also say what they do not know when there is no answer to read: a timeout, a dropped connection, a body that stopped halfway. Pingen may well have taken the letter before the line went dead, so a bare "the request failed" — which is what a retry gets triggered by — would be a guess in the direction of a second letter, printed and charged. Instead the error says the letter may be on its way and names the tool that can settle it: pingen_get_letter after a submit, where the id is in hand, and pingen_list_letters after an auto_send: true create, where it is not, because the id was in the answer that went missing.

Two things — and only two — take that warning off again, because a warning worth ignoring is worth nothing. Pingen answering for itself takes it off: a 404, a conflict_state, anything below 500 says the request was stopped and no paper moved. Never having sent the request takes it off too, in the same direction: a missing client secret or a refused token grant is a failure this server reaches on its own, with the line to Pingen never opened, and "Keine Pingen-Credentials" used to come back with a paragraph about a letter that might be printing. Everything else is genuinely unknown and keeps the warning — including a 502, 503 or 504, which look like an answer and are not: those are written by whatever sits in front of the API saying it could not get one back, which covers the letter already on the press exactly as well as the letter that never existed.


Tool reference

Tool

Parameters

What it does / returns

pingen_status

Verifies credentials; returns your organisations (id, name, plan, status) and the active org id. That list paginates too: if it is only a page, the result says truncated: true, and a configured PINGEN_ORG_UUID that is simply not on that page is no longer announced as belonging to no reachable organisation.

pingen_list_letters

limit (number, 1–100, default 20)

Lists recent letters, newest first: id, status, delivery_product, recipient, tracking, pages, submitted, price. One page only: if Pingen has more, the result also carries truncated: true — so "nothing went to that address" is never concluded from a list that was never read to the end.

pingen_send_letter

file_path (required, absolute PDF path); delivery_product (optional for a draft, required with auto_send: true); address_position (left|right, default left); auto_send (bool, default false)

Uploads the PDF and creates a letter. DRAFT by default — nothing is mailed. Returns the created letter row plus a note. Set auto_send: true to mail immediately; without a delivery_product that call is refused before anything is uploaded, because nothing asks for a product later and the letter would be franked however Pingen falls back. The note is decided by the status Pingen answered with and not by the flag that was sent, so a letter Pingen has already taken for printing is never reported as one still sitting there — and a status this server does not recognise, or an answer carrying none, is reported as unknown in both directions — neither as a send nor as "not a draft", because guessing the second shuts the two-step flow down just as confidently as guessing the first opens it. A resting status is not one answer either: action_required and invalid are Pingen refusing the PDF, so those are reported as a draft it will not take rather than as one waiting to be submitted — on both halves of the note, auto_send: true included, where "NICHT versandt" alone left the refusal unsaid and a second attempt looking sensible. The reason is named where it lives: pingen_letter_events. And when the auto_send: true call gets no answer at all — a timeout, a dead connection, or a 502/503/504 from a gateway that could not get one back either — the error says so rather than reading as a call that never arrived: the letter may already have been created and taken for printing, so it names pingen_list_letters for the check and warns against uploading the same PDF again.

pingen_submit_letter

letter_id (required); delivery_product (required); confirm: true (required); print_mode (simplex|duplex, default simplex); print_spectrum (color|grayscale, default color)

Physically mails an existing draft, at your cost, with no undo. Requires the letter to be valid. The result is decided by the status that came back rather than by the call having been accepted: only when Pingen says it has taken the letter is the row filed under submitted; otherwise it comes back as letter with a note saying so, so Pingen answering 200 while leaving it in action_required is reported as not on its way, and a status this server does not recognise — or none at all — is reported as unknown rather than as a send. Not every "still here" is the same either: action_required and invalid are Pingen refusing the PDF, so the note says a second attempt will not move the letter, points at pingen_letter_events for the reason and at a corrected PDF for the fix, while draft and valid still say to check the cause and send again. A failure with no answer behind it is not reported as a letter that stayed put either: Pingen may have taken it before the connection died, so the error points at pingen_get_letter and against a blind second send — and a 5xx counts as no answer, because it is a gateway or a broken handler saying so rather than Pingen saying what happened. What does not get that warning is a failure that settles the question: a status below 500, and a failure this server hit before the request went out — no credentials, a refused grant, an empty base. A connection that died once the request had left is not one of those, and keeps the warning: at the point where it reads fetch failed, a socket that was never opened and a socket that was closed after the letter reached the press look identical.

pingen_get_letter

letter_id (required)

Status/tracking of one letter (single letter row).

pingen_cancel_letter

letter_id (required)

Cancels an already-submitted/sent letter where Pingen still allows it. Returns { cancelled: <id> }.

pingen_delete_letter

letter_id (required); confirm: true (required)

Deletes a draft / not-yet-sent letter for good. To stop a letter already on its way use pingen_cancel_letter. Returns { deleted: <id> }.

pingen_letter_events

letter_id (required)

Tracking/status history (created → submitted → sent → delivered → undeliverable …): type, at, detail. First page only, newest first; truncated: true when Pingen reports more — and also when it reports nothing at all and the page came back exactly full, which is the last evidence available that the trail was cut off. So a letter that is still moving is never read as one that has stopped.

pingen_download_letter

letter_id (required); output_path (required)

Downloads the final letter PDF to output_path (available once processed/sent). Returns { saved, bytes }. Pingen answers this either with the letter's bytes or with a pointer to them, and which of the two it is, is decided by the body rather than by the Content-Type — a media type is case-insensitive, and an answer that carries none at all is defined to mean application/octet-stream, so reading the header literally turned a perfectly good letter into Unexpected token '%'. Nothing is written that does not start with %PDF-: a bucket answering an expired link with XML, or a portal answering with its sign-in page, is reported rather than saved under the name of a letter.

Example

// 1) create a DRAFT (nothing mailed yet)
pingen_send_letter { "file_path": "/Users/me/Einsprache_2024.pdf", "delivery_product": "registered", "address_position": "left" }
// → { created: { id: "<letter_id>", status: "draft", … },
//     note: "DRAFT erstellt (nichts versandt). Zum Senden: pingen_submit_letter." }

// 2) review in the Pingen dashboard, then physically mail it
pingen_submit_letter { "letter_id": "<letter_id>", "delivery_product": "registered", "print_mode": "duplex", "print_spectrum": "grayscale", "confirm": true }

Delivery products & print options (Switzerland)

Pass delivery_product to pingen_send_letter / pingen_submit_letter:

Value

Swiss product

Notes

cheap

B-Post

economy, slower

fast

A-Post

priority, next-day where available

registered

Einschreiben

tracked + signed-for delivery

premium

priority/premium

availability depends on plan

Print options on pingen_submit_letter:

  • print_mode: simplex (single-sided, default) or duplex (double-sided)

  • print_spectrum: color (default) or grayscale

Exact product availability depends on your organisation/plan — check the Pingen dashboard.


PDF layout gotcha (make your letters pass validation)

Pingen reads the recipient address optically from the first page and reserves a franking zone. If your PDF doesn't respect the Swiss letter window, Pingen returns action_required with protected_stamp_area and the letter can't be submitted. To pass validation:

  • Recipient address inside the address window. For address_position: left the window sits roughly from 60 mm down from the top, left column starting ~22 mm from the left edge. Put the full recipient block there.

  • Keep the franking zone (top ~40–60 mm) blank. No logo, no text, no line in the top strip — that area is reserved for the stamp/frank.

  • Do not put a sender return line inside the window. A return address in the same window confuses address recognition — keep only the recipient in the window (a sender line, if any, belongs above/outside it).

If in doubt, create a draft with pingen_send_letter, open it in the dashboard, and check the address preview before submitting.


Troubleshooting

Symptom

Cause

Fix

action_required / protected_stamp_area

Address outside the window, or something in the franking zone / a sender line inside the window

Reposition the recipient into the address window and clear the top ~40–60 mm (see PDF layout gotcha), re-upload. Which of them it was is on the letter's trail, not on the letter: pingen_letter_events shows the code Pingen rejected it with.

conflict_state when submitting

The letter isn't valid yet (still a draft that needs work)

Fix the address issue so the draft reaches valid, then call pingen_submit_letter. Check state with pingen_get_letter.

Keine Pingen-Credentials … / empty keychain

The security lookup returned nothing

Re-run the security add-generic-password commands with the exact service names, then verify with security find-generic-password -a pingen -s pingen-mcp-client-id -w.

Token error (401/400 on /auth/access-tokens)

Wrong Client-ID/Secret, or the OAuth client isn't a client_credentials client

Recreate the OAuth client with the client_credentials grant and re-store the values.

fetch failed / Zeitüberschreitung / 502, 503, 504 on send or submit

The request left, a usable answer did not — Pingen may have taken the letter anyway. A gateway status is not Pingen's answer, it is the box in front saying it could not get one

Do not repeat the call. Check first: pingen_get_letter for a submit, pingen_list_letters (newest first) for an auto_send: true create. Only then decide.

Keychain prompt on every start

macOS didn't remember the access grant

On the popup click Always Allow for node.


Releasing

Published from CI with npm Trusted Publishing (OIDC) — there is no npm token anywhere: no secret to store, rotate or leak. npm recommends this over an automation token, and is restricting tokens that bypass 2FA.

One-time setup per package, on npmjs.com -> the package -> Settings -> Trusted Publisher:

Field

Value

Organization or user

sapn95

Repository

pingen-mcp

Workflow filename

release.yml

Allowed actions

npm publish

The workflow filename must match exactly. That is deliberate: it stops any other workflow in the repo from publishing under your name.

Then every release is one command:

npm version patch && git push --follow-tags

The tag triggers the release workflow: it upgrades npm (trusted publishing needs

= 11.5.1 and Node >= 22.14), refuses a tag whose version disagrees with package.json, runs the gate, and publishes with a signed provenance statement.

If the publish fails with 404

npm notice publish Signed provenance statement ... from GitHub Actions
npm error 404 Not Found - PUT https://registry.npmjs.org/pingen-mcp

Provenance was signed, so OIDC worked — the registry simply does not accept this workflow as a publisher yet. That means the trusted publisher is not configured, or the repository / workflow name does not match. npm answers 404 rather than 403 so as not to reveal whether the package exists. It is not a credential problem: there is no credential, by design.

Checks

npm run gate
npm run mutate    # mutation-test the lines this branch changed

Runs exactly what CI runs, offline and without credentials: a syntax check, ESLint, the protocol smoke test, the hygiene scan, and the test suite under coverage. npm test runs just the suite, npm run lint just the linter.

The smoke test completes the MCP handshake over stdio and asserts the things that have actually broken here — a server version drifting from package.json, a tool in the dispatcher but missing from the tool list (or advertised and unhandled), a required property absent from a schema, and descriptions too thin to choose a tool from. The hygiene scan refuses secrets, tracked session files and personal identifiers.

The suite in test/ drives the server over stdio exactly as a real MCP client does, against a local stand-in for api.pingen.com (test/mock-pingen.mjs) that listens on an ephemeral port. Credentials are fake, PINGEN_API_BASE points at the mock, and a security stub that finds nothing goes first on PATH: no test can reach the real API, the real login keychain, or the post. Alongside the happy paths it pins the properties that matter — that pingen_send_letter creates a draft and submits nothing, that a non-boolean auto_send still yields a draft, that neither of the two calls that reach the post will do so without a delivery_product, that neither of them reports a send Pingen did not confirm — in the note or in the key the letter is filed under — that a draft Pingen has flagged is never announced as ready to post and never answered with an instruction to send it again, by any of the three branches that see that status — the two halves of pingen_send_letter and pingen_submit_letter — that each of them names a tool that can actually say why the letter was refused, that neither of the two calls that mail reports a request Pingen never answered as a letter that stayed put — whether the answer was silence or a gateway's 502 — while the two failures that do settle the question are left alone, a status Pingen wrote itself and a request that was never sent, that submitting is a PATCH, and that no token or client secret can appear in a tool result or on stderr even when the upstream error body quotes it back. The gate fails below 90% line, 90% function and 80% branch coverage of index.js.

test/hygiene.test.mjs points the hygiene scan at throwaway git repositories instead, because run over this repository — where everything is clean — a scan that silently skipped half the files would look exactly like one that worked.

Mutation testing

npm run mutate asks a different question from everything above: not "do the tests pass" but "would they notice if a guard were removed". StrykerJS deletes one piece of behaviour at a time and reruns the suite; whatever survives is something no assertion is watching.

That found eleven real gaps here after the model review rounds had stopped turning anything up — among them a path that walked out of the letters collection, a warning that went missing at exactly one HTTP status, and a page of one organisation being read as an account with one organisation. Most of the fixes were to the fixture rather than the code: a stand-in that accepts more than the real server does is a stand-in that hides the difference.

There is no browser in this suite, so npm run mutate:all over the whole file is about forty minutes and worth running. stryker.config.json explains every setting that is not a default — including why incremental mode is off, and why the number to watch when tuning it is the timeout count rather than the score.

License

MIT — see LICENSE.

Available Tools

9 tools
pingen_cancel_letterB

Cancel a letter that has already been submitted/sent (where cancellable).

ParametersJSON Schema
NameRequiredDescriptionDefault
letter_idYes

TDQS

B3.4/5.0
Behavior2/5

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

With no annotations, the description carries the full burden for behavioral disclosure. It only adds that cancellation is possible 'where cancellable', but does not disclose side effects, reversibility, failure modes, or whether this is a destructive operation. The caveat is helpful but insufficient for a mutation-like operation.

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 a single, clear sentence that states the core purpose without extraneous words. It is front-loaded and efficiently conveys the essential information, making it highly concise and well-structured.

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?

This is a simple one-parameter tool, and the description covers the primary action. However, without annotations or an output schema, it lacks details on what happens after cancellation, error conditions, or what 'cancellable' entails. It is minimally adequate but leaves gaps for the agent to handle unknown edge cases.

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

Parameters2/5

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

The schema has only letter_id with 0% description coverage. The description does not explicitly explain that letter_id identifies the letter to cancel, nor does it add format or source information. The parameter's meaning is inferable from context and the tool name, but the description adds no 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 action ('Cancel') and the object ('a letter that has already been submitted/sent'), which distinguishes it from sibling tools like delete_letter (likely for drafts) or submit/send. The specificity of 'submitted/sent' and the conditional 'where cancellable' adds clear scope, making the purpose unambiguous.

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 this tool is used when a letter has already been submitted/sent, which hints at the appropriate scenario. However, it does not provide explicit when-to-use vs alternatives, nor does it mention any exclusions or prerequisites (e.g., do not use for drafts, use delete_letter instead).

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

pingen_delete_letterA

DESTRUCTIVE: delete a draft / not-yet-sent letter for good. Requires confirm:true. To stop a letter already on its way use pingen_cancel_letter instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
confirmYes
letter_idYes

TDQS

A4.7/5.0
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It labels the action as 'DESTRUCTIVE', notes irreversibility with 'for good', and explains the confirm:true requirement. It does not mention permissions or error handling, but for a simple delete operation, this is strong transparency.

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 a single, front-loaded sentence with no wasted words. The 'DESTRUCTIVE' warning is immediately visible, and every clause adds essential information.

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?

For a simple 2-parameter delete tool with no output schema and no annotations, the description covers the destructive nature, the required confirmation, and the alternative tool for canceling in-transit letters. This is sufficient 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?

Schema coverage is 0%, so the description must compensate. It clarifies the confirm parameter by stating 'Requires confirm:true', and implies letter_id refers to the draft/not-yet-sent letter being deleted. However, it does not detail letter_id format or where to find it, which would add further value.

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: 'delete a draft / not-yet-sent letter for good.' It uses a specific verb (delete) with a specific resource (draft/not-yet-sent letter) and distinguishes itself from the sibling pingen_cancel_letter by noting the alternative use case.

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

Usage Guidelines5/5

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

The description explicitly provides usage context: use for drafts/not-yet-sent letters, and explicitly names the alternative pingen_cancel_letter for letters already in transit. This gives clear when-to-use and when-not-to-use guidance.

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

pingen_download_letterA

Download the letter PDF to output_path (available once the letter is processed/sent).

ParametersJSON Schema
NameRequiredDescriptionDefault
letter_idYes
output_pathYes

TDQS

A4/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. It discloses the key prerequisite (letter being processed/sent) but doesn't mention potential errors, file overwrite behavior, or that it saves to a local filesystem. This is minimal but sufficient for a simple download operation.

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 a single, highly informative sentence. It includes the action, target, destination, and a timing constraint without any redundancy or filler.

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 simple two-parameter tool with no output schema, the description covers the essential aspects: what it does, where it writes, and when it's available. It doesn't need to explain return values, and the lack of nested objects keeps it straightforward.

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. It explicitly explains output_path as the destination, but letter_id is left implicit, relying on the tool name for meaning. This partial clarification justifies a mid-range score.

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 action: 'Download the letter PDF to output_path'. It specifies the verb (download), the resource (letter PDF), and the destination (output_path), making it distinct from siblings like pingen_get_letter (which likely retrieves metadata) and pingen_list_letters.

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 parenthetical '(available once the letter is processed/sent)' provides clear timing guidance for when this tool should be used. It doesn't explicitly name alternatives, but the context and sibling names imply the appropriate scenario, earning a score above baseline.

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

pingen_get_letterB

Get one letter status/tracking by id.

ParametersJSON Schema
NameRequiredDescriptionDefault
letter_idYes

TDQS

B3.2/5.0
Behavior2/5

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

With no annotations, the description carries the full burden of behavioral disclosure. 'Get' implies a read operation, but there is no mention of error handling, read-only behavior, response structure, or limitations. The phrase 'status/tracking' only hints at the return content.

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 a single, concise sentence with no redundant words. It is front-loaded with the action and resource.

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

Completeness2/5

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

For a simple tool with one parameter and no output schema, the description is far too sparse. It does not provide enough context about expected response, usage timing, or how it differs from related tools, leaving the agent potentially under-informed.

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?

The input schema has 0% description coverage, so the description must compensate. 'By id' tells the agent that the parameter is the letter identifier, but it lacks format details, examples, or where to obtain the ID. This is minimal but not entirely absent.

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 action ('Get'), the resource ('letter'), and the scope ('status/tracking by id'). This distinguishes it from sibling tools like pingen_list_letters (multiple letters) and pingen_cancel_letter (mutation).

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?

No guidance is provided on when to use this tool versus alternatives such as pingen_status or pingen_letter_events. The description does not mention any exclusions or scenarios where this tool is preferred.

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

pingen_letter_eventsB

Tracking/status history of a letter (created, submitted, sent, delivered, undeliverable …).

ParametersJSON Schema
NameRequiredDescriptionDefault
letter_idYes

TDQS

B3.3/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 of behavioral disclosure. It adds value by enumerating event types (created, submitted, sent, delivered, undeliverable), giving a sense of what the tool returns. However, it does not explicitly state that the operation is read-only, nor does it describe response format, pagination, or ordering.

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 a single sentence that is front-loaded with the core purpose and includes relevant examples. Every word contributes meaning, with no fluff or redundancy.

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 simple tool with one parameter and no output schema, the description gives enough to understand the basic function. However, it omits details about the return structure (e.g., whether it returns a list of events, timestamps, or statuses) and potential error conditions. This is acceptable for basic use but leaves gaps for an agent.

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?

The schema has 0% description coverage, so the description must compensate for the sole parameter 'letter_id'. The phrase 'of a letter' implicitly indicates that the parameter identifies the letter whose history is being retrieved, but it does not explicitly state the parameter name, type, or constraints. This is adequate for a single obvious parameter but not exemplary.

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?

The description clearly identifies this as a tracking/status history tool for letters, listing example event types. It distinguishes itself from sibling tools like pingen_status (which likely provides current status) by emphasizing 'history' and 'events'.

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?

No guidance is provided on when to use this tool versus alternatives. It does not mention that pingen_status is for current status or that pingen_get_letter is for details. The reader must infer usage from the tool name and description.

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

pingen_list_lettersB

List letters with status and tracking. Optional page size.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNopage size (default 20)

TDQS

B3.4/5.0
Behavior3/5

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

With no annotations, the description must carry the burden of disclosure. It mentions 'with status and tracking' and optional page size, which hints at output content and pagination. However, it does not explicitly state that it is a read-only operation, nor does it describe pagination behavior beyond the existence of a page size.

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—two short sentences that immediately state the tool's purpose and its only parameter. No unnecessary words or redundancy.

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 simple list tool with one optional parameter and no output schema, the description covers the essentials: what is listed and the page size. It lacks details on return structure or filtering, but given the low complexity and sibling context, it is reasonably complete.

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 100% as the limit parameter is described as 'page size (default 20)'. The description's 'Optional page size' adds no additional meaning beyond what the schema already provides, so the baseline of 3 is appropriate.

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?

The description clearly states the tool lists letters and includes status and tracking, using a specific verb and resource. It is distinguishable from siblings like send or delete, though it does not explicitly contrast with get_letter or status.

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?

No guidance is provided on when to use this tool versus alternatives such as pingen_get_letter or pingen_status. The description only states what the tool does, leaving the user to infer appropriate usage.

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

pingen_send_letterA

Upload a PDF and create a letter. By default a DRAFT (auto_send=false) — nothing is mailed until pingen_submit_letter. Set auto_send=true to mail immediately; that path also requires delivery_product, because it is the one route that never reaches pingen_submit_letter and so nothing asks for a product later.

ParametersJSON Schema
NameRequiredDescriptionDefault
auto_sendNodefault false = create draft only
file_pathYesabsolute path to the PDF
address_positionNowindow position of the address on the first page (default left)
delivery_productNoe.g. cheap (B-Post), fast (A-Post), registered (Einschreiben), premium — CH values. Optional for a draft, required with auto_send=true

TDQS

A4.3/5.0
Behavior4/5

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

With no annotations, the description discloses the key behavioral distinction between draft and immediate send, and importantly explains why delivery_product is required on the auto_send path ('it is the one route that never reaches pingen_submit_letter'). It does not cover return values or error behavior, but the core send semantics are transparent.

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 dense sentences. The first states the main action, the second explains the draft/send branching and product requirement. Every clause adds information without padding.

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?

The tool has two distinct modes and 4 parameters, with no output schema. The description covers the modes and the parameter dependency, but omits what the operation returns (e.g., a letter ID) and any file validation details. This is a notable gap given there is no output schema to fill it.

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 already documents all 4 parameters, so the baseline is 3. The description adds valuable context connecting auto_send and delivery_product, explaining the logical reason for the dependency. This goes beyond the schema's individual field descriptions.

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 opens with 'Upload a PDF and create a letter', a precise verb+resource statement. It clearly distinguishes the draft/send behavior from the sibling pingen_submit_letter by explaining that drafts are not mailed until that tool is called, and auto_send=true creates the letter immediately.

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?

It explicitly explains the default draft workflow ('nothing is mailed until pingen_submit_letter') and the alternative immediate-send path (auto_send=true) including the delivery_product requirement. It does not explicitly list when not to use the tool, but the context is clear enough.

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

pingen_statusA

Verify credentials and show the active Pingen organisation (name, plan, id).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4/5.0
Behavior3/5

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

No annotations are provided, so the description must carry the burden. It states the tool verifies credentials and returns organisation details, but it does not disclose error behavior, side effects, or explicitly confirm that it is a read-only operation. The simplicity helps, but more detail on failure modes would improve transparency.

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 a single, well-structured sentence that immediately presents the verb and expected output. It is front-loaded and contains no extraneous information, every word earning its place.

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 simple status tool with no parameters and no output schema, the description sufficiently covers the key outputs (name, plan, id) and the primary action (credential verification). It could mention the response format or error handling, but the scope is simple enough that current coverage is adequate.

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 has zero parameters, and the schema is empty, so schema coverage is 100%. The description correctly avoids adding parameter details since there are none, meeting the baseline for a no-parameter tool.

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 purpose with specific verbs 'Verify' and 'show' naming both the credential check and the returned organisation fields (name, plan, id). This distinguishes it from the sibling tools, which all handle letter operations.

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 usage context is implied through the description, as it indicates credential verification and organisation status, which is distinct from the letter-focused siblings. However, it does not explicitly state when to use this tool versus alternatives or any exclusions, leaving the agent to infer the appropriate scenario.

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

pingen_submit_letterA

IRREVERSIBLE AND CHARGEABLE: prints and physically mails an existing DRAFT letter. Requires confirm:true. Optional print_mode (simplex/duplex) and print_spectrum (color/grayscale).

ParametersJSON Schema
NameRequiredDescriptionDefault
confirmYes
letter_idYes
print_modeNo
print_spectrumNo
delivery_productYes

TDQS

A4.4/5.0
Behavior5/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It explicitly warns that the action is 'IRREVERSIBLE AND CHARGEABLE,' states that it requires confirm:true, and details the physical mailing action. It also mentions optional print parameters. This is exemplary transparency for a side-effect-heavy tool.

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 a single, dense sentence that front-loads the most critical warnings ('IRREVERSIBLE AND CHARGEABLE') and efficiently packs in the action, the confirm requirement, and optional parameters. Every word earns its place with no fluff or redundancy.

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 tool with five parameters, no annotations, and no output schema, the description covers the essential operational context: action, risk, confirmation, and printing options. The main gap is the lack of explanation for the required 'delivery_product' parameter, which could confuse an agent about what values are acceptable. Otherwise, it presents a clear picture of what happens upon invocation.

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?

The schema has zero description coverage for parameters. The description adds meaning for confirm (requires true) and enumerates possible values for print_mode and print_spectrum. However, it leaves letter_id and the required delivery_product unexplained. While it adds value for three of five parameters, the required delivery_product is completely unaddressed, limiting the compensatory effect.

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: 'prints and physically mails an existing DRAFT letter.' This is a specific verb+resource, and it distinguishes the tool from siblings like pingen_send_letter (likely digital) and pingen_delete_letter by emphasizing physical mailing of a draft. The term 'submit' is clarified by the action of printing and mailing.

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 provides clear usage context: use when you need to physically mail an existing draft letter, with an explicit warning about irreversibility and chargeability. It implicitly distinguishes from alternatives by specifying 'DRAFT letter' and physical mailing, but it does not explicitly name alternatives or state when not to use this tool. It conveys the need for confirm:true.

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. 9 tool updatesv0.3.0
    • First observedpingen_cancel_letter
    • First observedpingen_delete_letter
    • First observedpingen_download_letter
    • First observedpingen_get_letter
    • First observedpingen_letter_events
    • First observedpingen_list_letters
    • First observedpingen_send_letter
    • First observedpingen_status
    • First observedpingen_submit_letter

TDQS

A3.9/5.0

Scored across 9 tools

Disambiguation4/5

Most tools target distinct operations: send, submit, get, list, download, cancel, delete, events, and status. pingen_get_letter and pingen_letter_events both relate to tracking, but the former gives current status while the latter gives history, so they are distinguishable with the provided descriptions.

Naming Consistency4/5

All tools share the 'pingen_' prefix and most follow verb_noun naming (e.g., pingen_send_letter, pingen_delete_letter). pingen_letter_events and pingen_status deviate from the verb pattern, but the overall convention is predictable and clear.

Tool Count5/5

Nine tools cover the full letter lifecycle—creating, submitting, tracking, downloading, cancelling, deleting—plus an org status check. The count is well-scoped and each tool serves a clear purpose without unnecessary bloat.

Completeness5/5

The tool set covers the complete workflow for sending physical letters: upload/create, submit, monitor events/status, download PDF, cancel sent letters, and delete drafts. No obvious gaps in the core domain of letter management.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables AI agents to send physical letters and postcards to over 200 countries using Solana cryptocurrency for payment. It provides tools for generating mail quotes, managing wallet balances, and automating physical correspondence directly through the Model Context Protocol.
    4
    49 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables sending emails (including mass emailing), querying, updating, and canceling delayed emails via the Resend API.
    -
  • F
    license
    A
    quality
    D
    maintenance
    Enables sending letters and MICR-encoded checks, managing contacts and templates, and verifying US/Canadian addresses via the PostGrid Print & Mail and Address Verification APIs from Claude.
    30
    1
    -
  • A
    license
    A
    quality
    B
    maintenance
    Local MCP server that generates print-ready PDF letters with DIN 5008 compliant address positioning for window envelopes, handling structured content offline without external APIs.
    7
    55 npm
    MIT