Atlas / MCP servers / oddbit / shrtnr

shrtnrBLOCK

mcp/oddbit/shrtnr

MCP server, REST API and typed SDKs for short links. Per-user ownership, bundle analytics. Self-hosted on Cloudflare Workers, free tier.

Verdict
BLOCK
Grade
D
Trust score
69 /100
Exposed tools
39 24r · 11w · 4d
Transport
streamable-http
License
Apache-2.0
Stars
30
01

Overview

From the repository's own README, as read at the audited commit. Badges and raw HTML are left out.

[](https://www.npmjs.com/package/@oddbit/shrtnr) [](https://pypi.org/project/shrtnr/) [](https://pub.dev/packages/shrtnr)

shrtnr is a self-hosted URL shortener you drive from code and from AI assistants, not only from a dashboard. Every deployment ships a REST API with an OpenAPI spec, typed SDKs on npm, PyPI and pub.dev, and a native MCP server that Claude, Copilot and any other MCP client connect to through OAuth on Cloudflare Access. Links belong to the person who created them, several slugs can point at one destination, and bundles roll the clicks of a whole campaign into one report. It runs on Cloudflare Workers and D1, inside the free tier.

[](https://oddb.it/shrtnr-deploy-top)

Who this is for

  • Teams that need per-person permissions, not a shared password. Sign-in runs through Cloudflare Access, so every teammate arrives with their own identity. Links and bundles record who created them, and only the creator can edit, disable or delete them. Everyone can read everything. API keys are issued per person and act as that person. Permission model.
  • Developers integrating links into an app. A REST API documented by an OpenAPI 3.1 spec, with a live reference at /_/api/docs on your deployment. Typed SDKs for TypeScript, Python and Dart, generated from that spec. Bearer keys with read and create scopes. Link creation is idempotent, and QR codes come ba
Read from source at commit a58d748d1565OBSERVED · 2026-10-08
02

Connect

Built from this server's own package name, version and transport as found in its source — not copied from anyone's documentation, so it cannot drift against a page we do not control. Replace the environment placeholders with a token scoped to the least it needs.

claude-code
claude mcp add shrtnr --env SHRTNR_TEST_API_KEY=${SHRTNR_TEST_API_KEY} -- npx -y @oddbit/[email protected]
claude-desktop
{
  "mcpServers": {
    "shrtnr": {
      "command": "npx",
      "args": [
        "-y",
        "@oddbit/[email protected]"
      ],
      "env": {
        "SHRTNR_TEST_API_KEY": "${SHRTNR_TEST_API_KEY}"
      }
    }
  }
}
03

Exposed tools (39)

24 read · 11 write · 4 destructive. Blast radius: 4 tools can delete or overwrite — an agent that can be talked into calling a tool can be talked into calling this one.

ToolRiskDescription
OSSreadsomething
_execute_actionwriteShorten the current tab with shrtnr
add_custom_slugwrite
add_link_to_bundlewrite
archive_bundleread
compare_linksread
create_bundlewrite
create_linkwrite
delete_bundledestructive
delete_linkdestructive
disable_linkwrite
disable_slugwrite
enable_linkwrite
enable_slugwrite
get_bundleread
get_bundle_analyticsread
get_clicks_by_countryread
get_clicks_by_deviceread
get_clicks_by_referrerread
get_dashboard_statsread
get_linkread
get_link_analyticsread
get_link_breakdownread
get_link_qrread
get_link_timelineread
get_total_clicksread
get_trending_linksread
healthread
list_bundle_linksread
list_bundlesread
list_bundles_for_linkread
list_linksread
list_links_by_ownerread
remove_link_from_bundledestructive
remove_slugdestructive
search_linksread
unarchive_bundleread
update_bundlewrite
update_linkwrite
04

Trust audit

BLOCKgrade D · trust 69/100 Do not install this without reading the findings. The audit found something that could harm you or your machine.

LayerWhat it checksResult
L0Provenance & inventoryPASS
L1Static analysis of the codeFAIL
L2Instruction surface (what it tells the agent)PASS
L3Class-specific surfaceWARN
L4Behavioural (sandbox)SKIPPED

What the source does

Filesystem
declared (3 observation(s))
Network
declared (8 observation(s))
Shell
none-observed
Dependencies
not all pinned
Secrets in source
none-found

Findings (25)

HIGHSupply chain · supply.typosquat · CWE-829, CWE-1357
browser-extensions/package.json
preact ~ react
Why it matters. dependency name one edit from a popular package
Fix. verify the package; likely typosquat
MEDIUMNetwork egress · net.raw_ip · CWE-200, CWE-319
scripts/test-sdks-e2e.sh:28
URL="http://127.0.0.1:${PORT}"
MEDIUMFilesystem / path · mcp.destructive_tools · CWE-22, CWE-59
delete_bundle, delete_link, remove_link_from_bundle, remove_slug
Why it matters. 4 tool(s) can delete or overwrite
Fix. prefer a read-only mode or scoped tokens; the page states the blast radius
LOWInventory / provenance · inv.hidden_file · CWE-1104
.dev.vars.example
.dev.vars.example
Why it matters. hidden member outside the usual dotfiles
Fix. review its purpose
LOWInventory / provenance · inv.hidden_file · CWE-1104
.node-version
.node-version
Why it matters. hidden member outside the usual dotfiles
Fix. review its purpose
LOWInventory / provenance · inv.symlink · CWE-1104
AGENTS.md
AGENTS.md
Why it matters. link not followed
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
src/__tests__/unit/client-api-error-toasts.test.ts:114
const factory = new Function(
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
src/__tests__/unit/client-script-escaping.test.ts:42
const factory = new Function("document", code) as (doc: unknown) => {
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
src/__tests__/unit/client-settings-toasts.test.ts:34
const factory = new Function("api", "toast", "t", "document", "window", code) as (
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
src/__tests__/unit/client-t-replacer.test.ts:25
const factory = new Function(code) as () => (key: string, params?: Record<string, unknown>) => string;
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWFilesystem / path · fs.traversal · CWE-22, CWE-59
sdk/typescript/tests/e2e/e2e.test.ts:14
import { ShrtnrClient } from "../../src";
LOWFilesystem / path · fs.traversal · CWE-22, CWE-59
src/__tests__/admin/widgets/cache-invalidation.test.ts:5
import { applyMigrations, resetData } from "../../setup";
LOWFilesystem / path · fs.traversal · CWE-22, CWE-59
src/__tests__/admin/widgets/cache-invalidation.test.ts:6
import { getCacheVersion } from "../../../admin/widgets/cache";
LOWFilesystem / path · fs.traversal · CWE-22, CWE-59
src/__tests__/admin/widgets/cache-invalidation.test.ts:7
import worker from "../../../index";
LOWFilesystem / path · fs.traversal · CWE-22, CWE-59
src/__tests__/admin/widgets/cache.test.ts:5
import { applyMigrations, resetData } from "../../setup";
LOWNetwork egress · net.metadata_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:105
[".internal hostname", "http://metadata.google.internal/computeMetadata/v1/"],
Why it matters. cloud metadata endpoint: the classic SSRF credential grab
LOWNetwork egress · net.metadata_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:115
["link-local / cloud metadata", "http://169.254.169.254/latest/meta-data/"],
Why it matters. cloud metadata endpoint: the classic SSRF credential grab
LOWNetwork egress · net.metadata_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:131
["fully qualified .internal hostname", "http://metadata.google.internal./computeMetadata/v1/"],
Why it matters. cloud metadata endpoint: the classic SSRF credential grab
LOWNetwork egress · net.metadata_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:186
.mockResolvedValueOnce(redirectResponse("http://169.254.169.254/latest/meta-data/"))
Why it matters. cloud metadata endpoint: the classic SSRF credential grab
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:107
["IPv4 loopback", "http://127.0.0.1:8787/"],
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:108
["IPv4 loopback, other address in /8", "http://127.1.2.3/"],
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:111
["unspecified 0.0.0.0", "http://0.0.0.0/"],
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
src/__tests__/unit/title-fetch.test.ts:112
["RFC 1918 10/8", "http://10.0.0.5/"],
LOWObfuscation / stealth · obf.decode_call · CWE-506, CWE-94
src/access.ts:106
return JSON.parse(atob(parts[1]));
LOWSupply chain · supply.unpinned · CWE-829, CWE-1357
browser-extensions/package.json
@oddbit/shrtnr, preact, @testing-library/preact, @types/chrome, esbuild, happy-dom, typescript, vitest
Why it matters. 9 dependency range(s) float
Fix. pin exact versions or ship a lockfile

Gates applied: no_behavioural_pass.

Audited 2026-10-08 · audit v0.4.1 · source sha a58d748d1565full audit observations/trust-audit/mcp-server/oddbit__shrtnr.json · Report an issue / request a re-scan
05

Audit history

Every audit this server has had. A grade with a past is a grade somebody is still checking.

DateSourceVerdictGradeScoreChange
2026-10-08a58d748d1565BLOCKD69first audit
06

Questions

What is the shrtnr MCP server?

MCP server, REST API and typed SDKs for short links. Per-user ownership, bundle analytics. Self-hosted on Cloudflare Workers, free tier.

What tools does shrtnr expose?

39 in total: 24 read-only, 11 that write, and 4 that can delete or overwrite (delete_bundle, delete_link, remove_link_from_bundle, remove_slug). Every one is listed on this page with its risk.

Is shrtnr safe to connect to an agent?

No — not without reading the findings first. The audit graded it D (69/100) and found 1 critical or high issue in the source. Each one is listed on this page with the file and line it is on. Separately from the audit: 4 of its tools can destroy data, so scope the token you give it to what you actually need.

What credentials does shrtnr need?

It reads SHRTNR_TEST_API_KEY from the environment. Give it a token scoped to the least it needs — an agent that can be talked into calling a tool can be talked into calling it with your credentials.

How does shrtnr run?

It speaks streamable-http, so it runs as a service you connect to over the network. It is published on npm as @oddbit/shrtnr at 1.2.1.

How current is this page?

The grade is for one exact copy of the source (a58d748d1565), read on 2026-10-08. The repository is watched and re-audited when it changes.

Advertisement