subiekt123-mcp
Stateless MCP server for invoicing in Subiekt 123 using the InsERT API, enabling tools to preview and create invoices, manage documents, and fetch exchange rates.
README
subiekt123-mcp
Stateless MCP server for invoicing in Subiekt 123 (InsERT API).
- SDK:
@modelcontextprotocol/server2.0.0, spec 2026-07-28 - Transport: stateless Streamable HTTP (
createMcpHandler) and stdio - Node ≥ 20, zod v4, ESM
The server keeps no state between requests - createMcpHandler builds a fresh McpServer instance per
HTTP request, so it scales horizontally without session affinity and without Mcp-Session-Id.
New here? Start with GETTING-STARTED.md - credentials, first run, connecting a client, and issuing your first invoice. This README covers architecture and reference detail.
Safety gate: HMAC token
create_invoice does not accept an invoice payload - only a token issued by a preview_* tool.
preview_<template>_invoice({month:"2026-08"})
→ preview (buyer, line items, totals, rate, warnings)
→ token: <payload>.<HMAC-SHA256> (valid for 10 minutes)
create_invoice({token})
→ signature + TTL verification
→ POST /documents
Why: in MCP it is the model that invokes a tool, and annotations.destructiveHint is only a hint to the
client - the spec states plainly that annotations do not change how the SDK runs a tool. The token binds
issuance to one specific, previously displayed document: changing even a single amount invalidates the
signature. The token carries the whole payload, so the server stays stateless.
HMAC key: ~/.config/subiekt123/preview-hmac.key (32 random bytes, generated on first use, chmod 600).
It lives outside credentials.json - a token leaked from a conversation transcript is not enough to
forge approval.
Installation
cd subiekt123 && npm install
The repository contains no business data - no account numbers, no tax IDs, no amounts, no client
names. All of that lives in two files under ~/.config/subiekt123/ (chmod 600).
1. Secrets - credentials.json
{
"clientId": "<Client ID from the InsERT developer portal>",
"clientSecret": "<Client Secret>",
"redirectUri": "http://localhost:53682/callback",
"subscriptionKey": "<Subiekt 123 API subscription key>",
"subscriptionHeader": "Ocp-Apim-Subscription-Key"
}
Where to get them: konto.insert.com.pl → Moje produkty → InsERT API →
"Uzyskaj dostęp" → the Aplikacje menu → InsERT API
(developers.insert.com.pl). There you will find Moje aplikacje
(registration: scope subiekt123, return address = redirectUri) and Subskrypcje.
One-time OAuth consent:
npm run auth
The refreshToken rotates on every refresh and is persisted automatically. Inactivity for more than 90
days, or more than 365 days since the first consent, requires running npm run auth again.
2. Invoicing profile - profile.json
cp profile.example.json ~/.config/subiekt123/profile.json
chmod 600 ~/.config/subiekt123/profile.json
The profile describes the issuer, bank accounts, clients and invoice templates. Every template becomes an
MCP tool named preview_<key>_invoice, so the tool set depends on the profile rather than on the code.
The profile is validated at startup; a missing field or a reference to a non-existent client stops the
server with a specific message.
| Template field | Meaning |
|---|---|
key |
becomes part of the tool name: preview_<key>_invoice |
kind |
date rule: monthlyRecurring, sameDay, prepaid (see below) |
title, summary |
tool title and description as seen by the model |
client, bankAccount |
keys from the clients / bankAccounts sections |
currency |
PLN skips the rate; a foreign currency fetches the NBP rate automatically |
dueDays |
payment term in days, defaults to 14 |
defaultNetPrice |
default amount; omit it to force the model to pass an amount explicitly |
note, communityTransaction, jpkVatGroups |
optional document fields |
item |
the single line item: name, sourceProductId, vatRate, pkwiu, gtu |
Running
npm start # stdio - the MCP client spawns the process itself
npm run start:http # stateless HTTP on 127.0.0.1:3123/mcp
npm run inspect # MCP Inspector
Claude Code / Claude Desktop (stdio)
{
"mcpServers": {
"subiekt123": {
"command": "node",
"args": ["/path/to/subiekt123/src/stdio.mjs"]
}
}
}
HTTP
Bound to 127.0.0.1, with Host and Origin validation ahead of the handler
(localhostHostValidation, localhostOriginValidation) - a defence against DNS rebinding, where a
malicious site points its own domain at 127.0.0.1 so the browser treats the server as same-origin.
A foreign Origin gets 403. PORT changes the port, HOST the address.
Tools
| Tool | Annotations | Description |
|---|---|---|
list_documents |
read-only | Document list (0-indexed paging, pageSize ≤ 50) |
get_document |
read-only | Full document with line items |
list_clients / get_client |
read-only | Clients (get_client = 13 more fields) |
list_products |
read-only | Products |
get_exchange_rate |
read-only | NBP rate from the business day before the issue date |
preview_<key>_invoice |
read-only | One per profile template - dates and rate resolved automatically |
preview_invoice |
read-only | Arbitrary payload, outside the templates |
create_invoice |
destructive | Issues a document - from a token only |
delete_document |
destructive | Deletes a document (blocked once submitted to KSeF) |
print_document |
read-only | PDF as base64, optionally duplicate / ecoMode |
Date rules (template.kind)
The rule lives in code (dates.mjs); a profile template merely selects it.
monthlyRecurring - recurring monthly invoice
| Field | Rule |
|---|---|
issueDate |
today (overridable via the issueDate parameter) |
deliveryDate, taxLiabilityDate |
last day of the billed month (the month parameter) |
dueDate |
+dueDays from the issue date; weekend → Monday (Sat +2, Sun +1) |
exchangeRate |
NBP rate from the last business day before the issue date |
Never backdate the issue date - KSeF reports an error when issueDate precedes the day of
submission. Hence the default of today rather than the 1st of the month (user's decision, 2026-08-03).
For the same reason a foreign-currency invoice cannot be issued ahead of time - the rate from the day
before the issue date does not exist yet.
sameDay - one-off invoice
Issue date = sale date = the given date (defaults to today), due +dueDays with the weekend adjustment.
prepaid - invoice for a payment already received
Due date = issue date, and payments is filled automatically with the full amount.
Always confirm the sale date with the user - it is the day the payment arrived, which may be earlier
than the issue date. A sale date later than the issue date is rejected.
Public holidays are not shifted - weekends only (user's decision, 2026-07-31).
API pitfalls (confirmed empirically)
calculationMethodmust match the company's "Licz od" setting - otherwise the fiscalization status diverges. WithNetpassnetPrice, withGrosspassgrossPrice.exchangeRateis mandatory for foreign currencies. The API does not fetch the rate itself (the UI does) - without it you getEXCHANGE_RATE_MUST_BE_SET_ON_DOCUMENT_IN_FOREIGN_CURRENCY.items[].symbolconflicts withsourceProductId-DOCUMENT_ITEM_SYMBOL_MUST_BE_EMPTY. The symbol comes from the product record.vatRateis a string:"23%"or a non-percentage rate such as"o.o."(reverse charge).eInvoiceis a document type, not a KSeF submission. AfterPOSTthe invoice hasinvoiceMode: "Ksef"butksefNumber: null. Submission happens in the UI - the public API has no endpoint for it.- A KSeF invoice cannot be printed before submission -
DOCUMENT_MUST_BE_PROCESSED_BY_KSEF_BEFORE_PRINTING. The same error is returned for a non-existentid. - An invoice for a natural person requires a PESEL (
tinKind: "Pesel"+tin), otherwise it is created as a company. The UI does not require it - the discrepancy was reported to InsERT, with no response. pageNumberis 0-indexed,pageSizemaxes out at 50 (60 → 400).POST /documentsreturns a nested{"id":{"value":"..."}}, without the document number./printingrequiresContent-Type: application/jsoneven with an empty body - otherwise 415. It returns a binary PDF.- Numbers freed by deleted documents are reused.
- The public API does not list correction invoices (KFS) - they are visible only in the internal
gw/api.
Layout
src/
├── server.mjs McpServer factory - tool registration
├── stdio.mjs stdio entry point
├── http.mjs stateless HTTP entry point
├── auth.mjs one-time OAuth PKCE consent
└── lib/
├── config.mjs credentials.json
├── profile.mjs profile.json - reading and validation
├── api.mjs OAuth + HTTP client (JSON and binary)
├── token.mjs HMAC preview → create
├── dates.mjs date rules, weekend adjustment
├── nbp.mjs NBP exchange rates
├── invoice.mjs validation, totals, preview
└── templates.mjs assembling a payload from a profile template
profile.example.json profile template (placeholders, no real data)
GETTING-STARTED.md setup walkthrough, client wiring, troubleshooting
docs/api-notes.md full API documentation gathered empirically
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.