schengen-calculator
Rolling-window (Schengen 90/180) and per-entry visa day counting. Zero dependencies, TypeScript,works in the browser and on the server.
npm i schengen-calculator
import { countDays, SCHENGEN } from "schengen-calculator";
const result = countDays(
[
{ start: new Date("2026-01-05"), end: new Date("2026-01-20"), code: "FR" },
{ start: new Date("2026-03-01"), end: new Date("2026-03-14"), code: "IT" },
],
SCHENGEN,
);
result.used; // 30
result.remaining; // 60
result.mustLeaveBy; // the date you have to be out by
result.entries; // continuous presences, after merging
Use it from an AI agent (MCP)
The package is also an MCP server. Add it to Claude Desktop, Claude Code, Cursor or any MCP client:
{
"mcpServers": {
"schengen": { "command": "npx", "args": ["-y", "github:eladkishon/schengen-calculator"] }
}
}
Tools:
calculate_schengen_dayscounts days used and left under the 90/180 rule. Runs locally.check_visa_requirementreturns the entry type and allowed stay for a passport and destination.create_trip_plansaves a planned itinerary and returns a link the traveller opens to keep it in NomadPoint.
The last two call NomadPoint's public server, which needs no account or key. To skip the install,point a client that speaks Streamable HTTP straight athttps://gxhdcvoveufbkthhendf.supabase.co/functions/v1/mcp-public (registry name app.nomadpoint/travel-facts).
Why this exists
Counting days sounds like subtraction and isn't. These are the cases a home-made calculator getswrong, each of which is a real conversation with a border officer:
Entry and exit days both count. The 1st to the 10th is ten days, not nine. This singleoff-by-one is the most common bug in Schengen calculators.
Two records of one trip is still one trip. A flight arrival and a hotel booking describing thesame stay must not bill you twice. The rolling window counts unique days.
Moving inside the zone is one stay. Paris to Berlin does not start a new entry and does notdouble-count the day you travelled.
The gap between trips is not presence. If you fly home for February, February is not Schengentime. A counter that carries your last known country forward will invent weeks you never spent.
A per-entry allowance resets; a rolling one does not. US VWP gives you 90 days each entry.Schengen gives you 90 in any rolling 180 in total. Summing every past visit is wrong for thefirst; counting only the current visit is wrong for the second.
An arrival with no departure needs a bound. If all you know is that someone entered, presenceruns to the next position change — capped at one full allowance. Without the cap, one forgottenarrival implies years of continuous presence.
Days are UTC. Local midnight would make the answer depend on the machine's timezone.
API
countDays(spans, allowance, asOf?) → DayCount | null
spans are periods of presence. end: null means "arrived, no known departure".
interface PresenceSpan { start: Date; end: Date | null; code: string } // ISO 3166-1 alpha-2
null is returned when there is nothing to count — no presence in the zone inside the window.
presenceIntervals(spans, zone, asOf, maxOpenStayDays?)
The merged continuous presences, if you want them without the counting.
Built-in allowances
SCHENGEN — 29 states, 90 days in any rolling 180.CA4 — Guatemala, El Salvador, Honduras, Nicaragua: 90 days pooled, per entry.
Anything else is a literal:
const US_VWP = { zone: ["US"], allowed: 90 }; // per entry
const B1B2 = { zone: ["US"], allowed: 180 }; // per entry
Accuracy
This is arithmetic over dates you supply, not immigration advice. It does not know your visa, yourresidence permit, exemptions, or the officer's discretion. Rules change. Check with the authoritythat issues the stamp.
Where it came from
Extracted from NomadPoint, a trip planner that tracks entry rules forevery traveller on a trip, across 39,601 passport-and-destination combinations. The counting engineis the same one running in the app; publishing it seemed more useful than everyone rewriting theoff-by-one.
MIT.