# Shlink MCP Server — 23 tools via DADL

The Shlink DADL turns Shlink's API into an MCP server that Claude, GPT or any MCP-compatible agent can consume directly. One YAML file declares all 23 tools — tag, short, domain, visit, orphan, redirect, and more — and ToolMesh serves them at runtime. No Python boilerplate, no per-endpoint code, no separate MCP server process.

Below: the endpoint coverage matrix, a two-block ToolMesh setup, the full tool reference grouped by Shlink feature area, required credential scopes.

**Source:** [Shlink REST API](https://api-spec.shlink.io/)

**Updated:** 2026-10-23

**Tags:** cloud, automation, crud, logging, user-management, analytics, file-management, monitoring, auth:apikey

## Which Shlink endpoints are covered?

**88%** (23 of ~26 endpoints).

**Focus:** Shlink REST API v3: short URLs (custom slugs, tags, expiry, visit limits, title, query forwarding), visits (per short URL, tag, domain, orphan visits, bot filtering, visit summaries), tags (stats, rename), redirect rules (device, language, query param, IP address, geolocation, date, browser), domains (not-found redirects), health.

**Missing:** The public redirect endpoint /{shortCode} and the tracking pixel /{shortCode}/track (visitor-facing, both create real visits), the deprecated GET /short-urls/shorten (API key in the query string), and multi-tag filters with tagsMode/excludeTagsMode (one tag per call). API key management, imports and GeoLite updates exist only in the Shlink CLI; QR codes were removed from Shlink in 5.0.


*Last reviewed: 2026-10-23*

## How do you configure the Shlink DADL?

1. Shlink API keys are created with the Shlink CLI on the server, not in a UI. For the official Docker image run: docker exec -it <shlink-container> shlink api-key:generate --name=toolmesh
2. Copy the printed key right away. Shlink stores keys hashed and cannot show the key again.
3. A key without roles is an admin key and works with every tool here. For least privilege add --author-only (only short URLs this key created), --domain-only=s.example.com (one non-default domain) or --no-orphan-visits; such keys cannot rename or delete tags or set domain redirects.
4. Optional expiry: --expiration-date=2027-12-31. Revoke a key later with shlink api-key:disable <name>.
5. Fresh container without shell access: start it once with INITIAL_API_KEY=<key-of-your-choice> to seed an admin key.
6. Put the key into the ToolMesh .env as CREDENTIAL_SHLINK_API_KEY and add the backends.yaml entry with your instance URL.

**Environment variable:** `CREDENTIAL_SHLINK_API_KEY`

[Authentication docs](https://shlink.io/documentation/api-docs/authentication/)

*Shlink is self-hosted: set url to the origin that serves /rest of your instance (usually the short domain, e.g. https://s.example.com) without a /rest suffix. Do not use the address of the Shlink web client (admin UI): it answers every request with its HTML start page and HTTP 200, so the tools return HTML instead of JSON. Check with https://<your-host>/rest/health, which must return JSON with status pass. Requires Shlink 3.0 or newer for REST API v3; redirect rules need 4.0, the date conditions 5.0 and the browser condition 5.1. Verified against Shlink 5.1.8. Geolocation fields and the geolocation redirect conditions need a GeoLite2 license key on the Shlink server. The health endpoint needs no key.
*

## How do you install the Shlink MCP server with ToolMesh?

Add to your `backends.yaml`:

```yaml
- name: shlink
  transport: rest
  dadl: shlink.dadl
  url: "https://s.example.com"

```

Set the credential:

```
CREDENTIAL_SHLINK_API_KEY=your-token-here
```

## What 23 tools does the Shlink DADL expose?

- **GET** `list_short_urls` — List short URLs, newest first by default. Returns {data: ShortUrl[], pagination: {currentPage, pagesCount, itemsPerPage, itemsInCurrentPage, totalItems}}; page through with page. Each ShortUrl has shortCode, shortUrl, longUrl, dateCreated, tags, meta {validSince, validUntil, maxVisits}, domain (null = default domain), title, crawlable, forwardQuery, hasRedirectRules and visitsSummary {total, nonBots, bots}. Expired or exhausted links are included unless the exclude* flags are set. Tag filters take ONE tag each (Shlink needs the PHP array key "tags[]"; call again for another tag).

- **POST** `create_short_url` — Create a short URL and return the ShortUrl object (its shortUrl field is the link to hand out). Without customSlug Shlink generates a 5-character code (shortCodeLength >= 4). customSlug is slugified: spaces and "/" become "-". Tags are lowercased and slugified. A slug already used on that domain fails with 400 non-unique-slug; invalid input fails with 400 invalid-data listing invalidElements. Set findIfExists=true to get an existing short URL for the same long URL (and same optional fields) back instead of creating a duplicate.

- **GET** `get_short_url` — Get one short URL by short code: longUrl, tags, meta {validSince, validUntil, maxVisits}, title, crawlable, forwardQuery, hasRedirectRules and visitsSummary {total, nonBots, bots}. Short URLs on a non-default domain need the domain parameter, otherwise 404 short-url-not-found.

- **PATCH** `update_short_url` — Edit a short URL. True partial update: only the fields sent change, omitted fields stay as they are. null clears validSince, validUntil, maxVisits or title. tags REPLACES the whole tag list (send the full list; [] removes all tags). The short code and domain cannot be changed. Returns the updated ShortUrl. Short URLs on a non-default domain need the domain parameter.

- **DELETE** `delete_short_url` — Delete a short URL together with all of its visits. Irreversible; later hits on the code become orphan visits. Returns nothing (204). Fails with 422 when the server has DELETE_SHORT_URL_THRESHOLD set and the short URL has more visits than that. Short URLs on a non-default domain need the domain parameter.

- **GET** `list_redirect_rules` — List the dynamic redirect rules of one short URL. Returns {defaultLongUrl, redirectRules: [{longUrl, priority, conditions: [{type, matchKey, matchValue}]}]}; priority 1 is checked first and defaultLongUrl is used when no rule matches. Read this before set_redirect_rules when rules should be added rather than replaced.

- **POST** `set_redirect_rules` — Replace ALL redirect rules of a short URL (an empty array removes every rule). Priorities follow the array order. Each rule is {longUrl, conditions: [{type, matchKey, matchValue}]}; all conditions of a rule must match. Condition types: device (matchValue android, ios, mobile, windows, macos, linux, chromeos or desktop); language (matchValue such as en or en-US, from Accept-Language); query-param (matchKey = param name, matchValue = exact value); any-value-query-param (matchKey = param name, present with or without value); valueless-query-param (matchKey = param name, present without value); ip-address (matchValue exact IP, CIDR like 192.0.2.0/24 or wildcard like 192.0.2.*); geolocation-country-code (matchValue ISO 3166-1 alpha-2, e.g. DE); geolocation-city-name (matchValue city name); before-date and after-date (matchValue ISO-8601 date-time, Shlink 5.0+); browser (matchValue chrome, firefox, edge, safari, opera or android_browser, Shlink 5.1+). matchKey may be omitted or null where unused. Geolocation conditions need GeoLite2 on the server. Returns the resulting rule list like list_redirect_rules; an unknown value fails with 400 invalid-data whose invalidElements holds the index of the bad rule.

- **GET** `list_tags` — List tag names, alphabetically by default. Returns {data: string[], pagination}. Without itemsPerPage all tags come back in one page. Tags stay listed after their last short URL dropped them until delete_tags removes them; use list_tag_stats for usage counts.

- **GET** `list_tag_stats` — List tags with usage statistics. Returns {data: [{tag, shortUrlsCount, visitsSummary: {total, nonBots, bots}}], pagination}. Ordering by shortUrlsCount or visits is as slow as loading the whole list, so omit itemsPerPage when sorting by those.

- **PUT** `rename_tag` — Rename a tag on every short URL that carries it. Returns nothing (204). 404 tag-not-found when oldName does not exist, 409 tag-conflict when newName is taken (tags cannot be merged this way), 403 forbidden-tag-operation for non-admin API keys.

- **DELETE** `delete_tags` — Delete ONE tag (removes it from every short URL; the short URLs stay). Returns nothing (204), also for unknown names. Call once per tag. Non-admin API keys get 403 forbidden-tag-operation.

- **GET** `get_visit_stats` — Instance-wide visit counters: {nonOrphanVisits: {total, nonBots, bots}, orphanVisits: {total, nonBots, bots}}. Non-orphan visits hit an existing short URL; orphan visits hit the bare domain, an unknown or expired short code, or any other path. Cheap; use it before pulling visit lists.

- **GET** `list_short_url_visits` — List the visits of one short URL, newest first. Returns {data: Visit[], pagination}; a Visit is {referer, date, userAgent, visitLocation {countryCode, countryName, regionName, cityName, latitude, longitude, timezone} or null, potentialBot, visitedUrl, redirectUrl}. 100 per page unless itemsPerPage says otherwise. For counts per day, referrer or country use the summarize_visits composite instead of paging raw visits.

- **GET** `list_tag_visits` — List the visits of all short URLs carrying a tag, newest first. Returns {data: Visit[], pagination}, 100 per page by default. 404 tag-not-found for an unknown tag.

- **GET** `list_domain_visits` — List the visits of all short URLs on one domain, newest first. Returns {data: Visit[], pagination}, 20 per page by default. Use DEFAULT as domain for the default domain. 404 domain-not-found for an unknown domain. EXPENSIVE on instances with a large visits table: MySQL may walk the whole table to fill a page, and a 500-item page has exhausted a memory-limited database. Keep itemsPerPage small (<= 20) and prefer list_short_url_visits or list_tag_visits for analysis.

- **GET** `list_non_orphan_visits` — List visits to any existing short URL across the instance, newest first. Returns {data: Visit[], pagination}, 100 per page by default. Fast without the domain filter (index on the short URL reference). The domain filter adds the same join as list_domain_visits and can make MySQL walk the whole visits table; use small pages with it.

- **GET** `list_orphan_visits` — List orphan visits (requests that matched no valid short URL), newest first. Returns {data: OrphanVisit[], pagination}, 100 per page by default; each item is a Visit plus type: base_url (the bare domain), invalid_short_url (a single path segment that is no valid short code, i.e. unknown, expired or exhausted codes and scanner probes such as /wp-login.php) or regular_404 (any other path, e.g. several segments). Scanners and bots usually dominate here. API keys with the NO_ORPHAN_VISITS role always get an empty list. Every page runs a COUNT over all matching orphan visits; the domain filter (a substring match on visitedUrl), excludeBots and type turn that into a table scan. On instances with many orphan visits take totals from get_visit_stats.

- **DELETE** `delete_short_url_visits` — Delete ALL visits of one short URL (the short URL itself stays and its counters restart at 0). Irreversible. Returns {deletedVisits: number}.

- **DELETE** `delete_orphan_visits` — Delete ALL orphan visits of the instance (bot and scanner noise on unknown paths). Irreversible; there is no date or type filter. Returns {deletedVisits: number}; keys with the NO_ORPHAN_VISITS role get 0.

- **GET** `list_domains` — List the domains in use: {data: [{domain, isDefault, redirects: {baseUrlRedirect, regular404Redirect, invalidShortUrlRedirect}}], defaultRedirects}. defaultRedirects apply to every domain without its own setting; null means Shlink shows its 404 page. A domain appears once a short URL or a redirect setting uses it.

- **PATCH** `set_domain_redirects` — Set where visitors of a domain go when nothing matches: the bare domain (baseUrlRedirect), a single path segment that is no valid short code, e.g. an unknown or expired one (invalidShortUrlRedirect), or any other path (regular404Redirect). Merges with the current values: omitted fields stay, null clears one. Creates the domain entry if needed. Needs the real authority (DEFAULT is rejected) and an admin API key. Returns the resulting redirects.

- **GET** `get_mercure_info` — Get the Mercure hub URL and a short-lived subscriber JWT for real-time updates (new visits, new short URLs): {mercureHubUrl, jwt, jwtExpiration}. The JWT is a credential; do not log or share it. 501 mercure-not-configured when the instance has no Mercure hub.

- **GET** `get_health` — Health check of the instance including its database connection: {status: pass|fail, version, links}. HTTP 503 with status fail when Shlink cannot reach its database. Needs no API key; version shows the Shlink release. An HTML page instead of JSON (here and from every other tool) means the backend url points at the Shlink web client, not at the Shlink server.


## What composite workflows does the Shlink DADL provide?

- **FN** `summarize_visits` — Aggregate visits instead of paging raw rows: counts per day, top referrer hosts, countries, cities, visited and redirect URLs, bots and orphan types. scope selects the source: short_url (needs shortCode, plus domain for a non-default domain), tag (needs tag, optional domain), non_orphan (all short URLs) or orphan. Reads newest first in pages of 500, at most maxVisits rows (default 5000, max 20000); truncated=true means the counts cover only the newest analyzed visits of total_matching. There is deliberately no per-domain scope: that query can make MySQL walk the whole visits table. On instances with many orphan visits every orphan page counts all of them, so keep maxVisits low there and take totals from get_visit_stats.


## Which DADLs are related to Shlink?

- [Xen Orchestra](https://www.dadl.ai/d/xen-orchestra/) — Xen Orchestra REST API (XO 6.4+, current through 6.10) -- complete coverage: VMs (incl. PATCH update + snapshot revert + forced migration), VM controllers, hosts (incl. full power/lifecycle actions + maintenance mode + PIF rescan + SR probing of NFS/iSCSI/HBA/ZFS targets), pools (incl. add_host, rolling reboot/update with interrupted-update recovery + finalize), storage (SR create/delete, VDI incl. PATCH update + vhd/raw/qcow2/vmdk export, VBD), networks (VIF incl. PATCH update + PIF/PBD), VM/VDI snapshots, VM templates, hardware (PCI/PGPU/SM), tasks, backups (archives incl. live mount of backup disks, jobs/logs, repositories incl. health + benchmark + space reclaim), schedules, messages, alarms, events (SSE incl. non-XAPI collections and backup archives), RBAC v2 (users/groups/acl-roles/acl-privileges incl. assignment introspection), XO plugins (list, load/unload, autoload, self-test), proxies, servers, dashboards, SDN traffic rules (add/delete/update incl. OpenFlow priority), IPMI sensor inventory (plugin), audit records (plugin), health check
- [Billomat](https://www.dadl.ai/d/billomat/) — Billomat REST API — German cloud invoicing & accounting: invoices, estimates, credit notes, recurring invoices, delivery notes, order confirmations, dunning reminders, clients, contacts, articles, payments, expenses, taxes
- [Placetel](https://www.dadl.ai/d/placetel/) — Placetel REST API v2 — German cloud telephone system (hosted PBX): call log and voicemails, click-to-dial, call detail records, call recordings, contacts and caller lookup, SMS and fax, phone numbers and number profiles, call routing (forwards, ring groups, IVR menus, call center queues, routing plans, time settings), SIP users and short codes, users and permissions, announcements (text-to-speech prompts), call center agents and queues, CTI call control of desk phones, phone provisioning, call event webhooks, Microsoft Teams direct routing and Webex Calling.

- [Graylog](https://www.dadl.ai/d/graylog/) — Graylog REST API -- log search (Views/Search + legacy universal), streams, pipelines, inputs, alerts, events, dashboards, users, roles, sidecars, index management, and cluster administration. Targets Graylog 6.x.
- [Mikrotik](https://www.dadl.ai/d/mikrotik/) — MikroTik RouterOS REST API -- manage interfaces, IP addresses, routing, firewall, DHCP, DNS, PPP, queues, wireless, system configuration, users, certificates, files, logs, and diagnostics on RouterOS v7.1+ devices
- [NetBox](https://www.dadl.ai/d/netbox/) — NetBox DCIM/IPAM API -- full v4 coverage: sites, racks, devices, modules, interfaces, cables, power, IPAM (prefixes, IPs, VLANs, VRFs, route-targets, VLAN translation), virtualization, circuits (including virtual circuits), tenants, contacts, VPN (IKE/IPSec/L2VPN), wireless, extras (webhooks, event-rules, scripts, config-templates, bookmarks, notifications), users/permissions/tokens, and core data sources & jobs

---

**Canonical URL:** https://www.dadl.ai/d/shlink/
**Raw DADL:** https://github.com/DunkelCloud/dadl-registry/blob/main/shlink.dadl
