npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

portlandmaps-mcp

v0.2.5

Published

Unofficial Model Context Protocol server for the public PortlandMaps API (Portland, OR property, permit, zoning, sewer and parcel data). Not affiliated with or endorsed by the City of Portland.

Readme

PortlandMaps MCP Server

Standalone Model Context Protocol server for querying PortlandMaps /api/* endpoints from MCP clients such as Claude Desktop, Cursor, or Codex.

Unofficial / not affiliated. This is a community project that wraps the public PortlandMaps API. It is not affiliated with, authored by, or endorsed by the City of Portland or the PortlandMaps service. "PortlandMaps" is used here only to describe the upstream API this server connects to. Data is provided as-is; see the Important Caveat below.

Install

Run the published server directly with npx (no clone/build needed):

PORTLANDMAPS_API_KEY="your-key" npx -y portlandmaps-mcp

Add it to an MCP client (Claude Desktop, Cursor, Codex, …):

{
  "mcpServers": {
    "portlandmaps": {
      "command": "npx",
      "args": ["-y", "portlandmaps-mcp"],
      "env": {
        "PORTLANDMAPS_API_KEY": "your-key"
      }
    }
  }
}

PORTLANDMAPS_API_KEY is the only required setting (see Setup for the optional ones). For a from-source / HTTP / Vercel run instead, see the sections further down.

Tools

  • suggest_portlandmaps_address - 1:1 wrapper for /api/suggest/.
  • get_portlandmaps_detail - 1:1 wrapper for /api/detail/.
  • get_assessor_data - 1:1 wrapper for /api/assessor/.
  • get_permit_data - 1:1 wrapper for /api/permit/.
  • search_landuse_cases - 1:1 wrapper for /api/landuse/.
  • search_sewer_records - 1:1 wrapper for /api/sewer/.
  • query_intersects - 1:1 wrapper for /api/intersects/.
  • convert_geometry - 1:1 wrapper for /api/geometry/.
  • lookup_agol - 1:1 wrapper for /api/agol/.
  • list_detail_types - static catalog of detail_type values and expected identifier formats for /api/detail/.
  • render_parcel_shape - returns an interactive UI component (mcp-ui UIResource) drawing a property's lot as inline SVG over a City of Portland aerial photo (showing the true roofline): the parcel outline, the building footprint(s) sitting on the lot, approximate setback dimensions, a north arrow, and faint neighbouring parcels for context. Accepts detailType+detailId (fetches geometry) or raw rings. Set basemap: "none" to drop the photo (dark vector render) and includeContext: false for just the bare outline.

The server intentionally avoids composite tools. Agents should compose endpoint calls themselves. render_parcel_shape is the one deliberate exception (see UI Components below).

All tools are read-only and carry MCP annotations (readOnlyHint, idempotentHint, openWorldHint) so hosts can treat them as safe reads. The server also advertises usage instructions in its initialize result, teaching clients the address → Taxlots Property ID → detail lookup chain.

Prompts

The server registers three prompts (surfaced as slash commands in capable hosts). Each takes a single address argument and emits a guidance message that steers the model through the resolve-then-look-up workflow — the model still drives the tools, so the prompts stay host-agnostic.

  • property-report — resolve an address, then compile assessment, zoning, and recent permits into a single report.
  • show-parcel — resolve an address, then render its parcel via render_parcel_shape.
  • zoning — resolve an address, then explain its base zone, overlays, and key development standards.

UI Components (MCP Apps + mcp-ui)

render_parcel_shape renders a property's lot outline as an interactive UI component. It targets two host families at once:

  • MCP Apps hosts (Claude): the tool declares _meta.ui.resourceUri pointing at a registered ui://portlandmaps/parcel-view.html template. The host renders the template in a sandboxed iframe and forwards the tool result; the bundled client (@modelcontextprotocol/ext-apps) reads structuredContent.rings and draws the SVG client-side.
  • Legacy mcp-ui hosts: the same tool result also embeds a ui:// resource with the baked HTML (built via @mcp-ui/server), which those hosts render directly. MCP Apps hosts ignore this embedded resource.

The lot outline is drawn from geometry.rings (Web Mercator / EPSG:3857) with a uniform scale (no distortion), Y-flip, and a cos²(latitude) area correction. The render math (src/parcelSvg.ts) is shared between the server and the iframe: an esbuild step (npm run build:template) bundles the client entry (src/client/parcelApp.ts) into a self-contained template emitted to src/parcelTemplate.generated.ts (regenerated by npm run build).

Context layers (footprints, neighbours, setbacks)

When includeContext is true (the default), the tool enriches the bare outline with data from two public Portland ArcGIS REST layers on the same host — no API key required — queried in the same EPSG:3857 frame so everything aligns without reprojection (src/contextLayers.ts):

  • Building Footprintsod/rest/services/COP_OpenData_Property/MapServer/48. Footprints intersecting a window around the lot are fetched; each is classified on- or off-lot by a centroid-in-polygon test, and the on-lot ones are highlighted. The largest on-lot footprint is the "primary" structure and carries BLDG_SQFT / NUM_STORY / YEAR_BUILT into the caption.
  • Regional Taxlotsarcgis/rest/services/Public/Taxlots/MapServer/0. Neighbouring lots are drawn faintly behind the subject for context.

Setbacks are the per-side gap (ground feet) between the primary building's bounding box and the lot's bounding box — a deliberately simple, honest approximation for the rectangular, street-aligned lots that dominate Portland; they are labelled "approx." and omitted when no on-lot building is found. The view frames on the subject lot (expanded ~45%) so a single oversized neighbour can't shrink it; geometry extending past the frame is clipped to the card.

Context fetching is best-effort: any failure (network, layer change, no footprint on the lot) degrades gracefully to the plain parcel outline, so the core render never breaks. Pass includeContext: false to skip the extra queries.

Aerial basemap (true roofline)

When basemap is "aerial" (the default), the tool draws a City of Portland aerial photo behind the vectors so the property's real roofline is visible. This matters because every available vector building-footprint layer returns a generalized rectangle (the assessor outline), not the true (often L-shaped) roof — the photo is the only honest way to show the actual structure.

  • SourcePublic/Aerial_Photos_Multi_Year (the most-recent imagery available per location, citywide; no baked cadastral lines or labels), fetched via the MapServer export operation. Public, same host, no API key.
  • Self-contained & aligned — the tool fetches the photo server-side for the exact EPSG:3857 projection window the vectors use (fetchAerialBasemap in src/aerial.ts requests export at the same expanded-lot bbox), then base64-encodes it and passes it through structuredContent as an inline data: URI. The sandboxed component renders <image href="data:image/jpeg…">; no external http(s) ref ever reaches the iframe (safeBasemap rejects any non-data: URI as a hard security boundary). A faint dark scrim keeps the light vector strokes legible, and a "Aerial: City of Portland" caption credits the basemap.
  • Footprints over the photo — because the assessor footprint is coarser than the photographed roof (and orthophotos lean elevated rooflines outward by a metre or two), footprints over the aerial are drawn as thin dashed schematics (no solid fill), so the real roof in the photo reads as the truth while the dashes still mark the subject structure and anchor the setback dimensions. In basemap: "none" mode they remain solid filled shapes.
  • Best-effort — a failed or empty (no-coverage) export degrades silently to the vector-only render. Pass basemap: "none" to skip the fetch entirely.

To verify the live MCP Apps render locally, build (npm run build) and connect the MCP Inspector to dist/index.js, then open the Apps tab and run render_parcel_shape. Note: the Inspector's Apps sandbox uses an OAuth handshake that 401s against a local stdio server, so launch it with auth disabled — DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector node dist/index.js — and the component renders as a live iframe. Pass rings directly to render without an API call, or a detailId (e.g. detailType: "zoning", detailId: "R337949") to fetch geometry.

ChatGPT (OpenAI Apps SDK) support is planned as a follow-on phase.

Setup

npm install
npm run build

Optional environment variables:

PORTLANDMAPS_API_KEY=...
PORTLANDMAPS_API_KEY_PARAM=api_key
PORTLANDMAPS_CACHE_TTL_MS=3600000

PORTLANDMAPS_API_KEY_PARAM defaults to api_key. Change it if PortlandMaps expects a different query parameter for the key you are using.

PortlandMaps documents api_key as required for API access. Without a key, some endpoints may return HTTP 200 with an error payload; this server converts those payloads into MCP tool errors.

The issued PortlandMaps key is rate limited to 200 requests per 15 minutes. PortlandMaps returns:

  • X-Rate-Limit-Limit
  • X-Rate-Limit-Remaining
  • X-Rate-Limit-Reset

If the limit is exceeded, PortlandMaps returns HTTP 429. This server includes those rate-limit headers in HTTP error messages.

Local Stdio Run

npm run dev

For MCP clients, use the built server over stdio:

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

Versioned HTTP Run

Use this when you need a remote MCP URL for Claude's custom connector flow. The HTTP server exposes a versioned endpoint so breaking changes can be registered as a fresh connector without uninstalling an old one.

npm run build
PORTLANDMAPS_API_KEY="..." npm run start:http

Defaults:

  • Health check: http://localhost:8787/health
  • Versioned MCP endpoint: http://localhost:8787/mcp/v2
  • Compatibility MCP endpoint: http://localhost:8787/mcp

Optional HTTP environment variables:

PORT=8787
MCP_VERSION=v2
MCP_MAX_BODY_BYTES=1048576    # cap on the request body buffered per call (default 1 MiB)
MCP_RATE_LIMIT_RPM=60         # per-client-IP request cap per window (default 60; 0 disables)
MCP_RATE_LIMIT_WINDOW_MS=60000 # sliding window length in ms (default 60s)
MCP_ICON_BASE_URL=https://portlandmaps-mcp.vercel.app  # origin serving the icon assets (empty disables icons)
MCP_WIDGET_DOMAIN=https://portlandmaps-mcp.vercel.app  # unique sandbox origin advertised for the parcel-view widget

Rendered widget metadata (CSP + domain)

The one rendered tool (render_parcel_shape) ships its UI as the ui://portlandmaps/parcel-view.html resource. App platforms expect that resource to declare a Content Security Policy and a unique sandbox domain; ChatGPT, for instance, shows "Widget CSP is not set" / "Widget domain is not set" submission warnings until both are present. src/widgetMeta.ts builds this _meta, advertised on both the resources/list entry and the resources/read content item, in two key conventions at once: the MCP Apps standard (_meta.ui.csp / _meta.ui.domain, consumed by Claude) and the OpenAI legacy aliases (openai/widgetCSP / openai/widgetDomain, consumed by ChatGPT).

The CSP is a deliberately empty allowlist (no connect/resource origins): the widget is fully self-contained — esbuild bakes the client into the template, the aerial basemap is an inline data: URI (never an external ref), and it talks to the host over postMessage — so it opens zero external connections. The domain has no functional job (no cross-origin requests need a CORS/OAuth allowlist); it exists only to give the app a unique, stable sandbox origin, which submission requires. Override it for a self-hosted deployment with MCP_WIDGET_DOMAIN (defaults to the Vercel origin).

Server icon / branding

The server advertises an icon set (serverInfo.icons) so capable clients — e.g. Claude custom connectors — and the MCP Registry listing can display a logo. The images live in public/ and are served at the site root (/favicon.svg, /favicon.png, /favicon-32x32.png, /favicon-16x16.png, /apple-touch-icon.png, plus a generated /favicon.ico and an index.html landing page). Per the MCP spec, each icon src must be an absolute HTTPS URL hosted on the deployment (a data: URI is not allowed), so MCP_ICON_BASE_URL must point at wherever public/ is served — it defaults to the Vercel domain. On Vercel the assets are served automatically from the output directory; the local dev:http / start:http server serves the same files from public/. The registry copy of this list lives in server.json under icons.

To rebrand: drop replacement images into public/ (keep the filenames, or update both src/icons.ts and server.json).

Public endpoint hardening

The HTTP endpoints are unauthenticated — anyone with the URL can call the read-only PortlandMaps tools using the server's PORTLANDMAPS_API_KEY. If you publish the URL (e.g. as a remotes entry in the MCP Registry or a public Claude connector), treat it as a free public proxy on your key and add guardrails:

  • Rate limiting (built in): every POST is throttled per client IP before any upstream PortlandMaps call, so a throttled request never burns your key quota. Tune with MCP_RATE_LIMIT_RPM / MCP_RATE_LIMIT_WINDOW_MS; set MCP_RATE_LIMIT_RPM=0 to disable. Blocked callers get HTTP 429 + Retry-After. Note: on serverless the limiter is per warm instance, so it bounds — but does not globally cap — traffic. Pair it with platform rate limiting (below).
  • On Vercel: enable a Spend Management cap, turn on Vercel Firewall rate-limit rules and/or BotID, and use a dedicated, rotatable PortlandMaps key so abuse is recoverable by rotating one credential.

For Claude custom connectors, deploy this HTTP server behind HTTPS and use:

https://your-host.example/mcp/v2

Do not put the PortlandMaps API key in Claude's connector UI. Keep it as a server-side environment variable on the host running this server.

Vercel Deploy

The repo ships with Vercel serverless functions in api/ so the same MCP server can be deployed without running a long-lived process. Each request creates a transport (sessionless mode), so it maps cleanly to a function invocation.

npm i -g vercel   # one-time
vercel link       # link to a project
vercel env add PORTLANDMAPS_API_KEY    # paste your PortlandMaps key
vercel deploy --prod

After deploy, the endpoints are:

  • https://<project>.vercel.app/health
  • https://<project>.vercel.app/mcp/v2 (versioned, use this for new connectors)
  • https://<project>.vercel.app/mcp (compatibility alias)

Optional env vars: MCP_VERSION, PORTLANDMAPS_API_KEY_PARAM, PORTLANDMAPS_CACHE_TTL_MS, MCP_RATE_LIMIT_RPM, MCP_RATE_LIMIT_WINDOW_MS, MCP_ICON_BASE_URL, MCP_WIDGET_DOMAIN. Note that both the in-memory cache (PORTLANDMAPS_CACHE_TTL_MS) and the in-memory rate limiter do not persist/share state across function instances on Vercel — see Public endpoint hardening and pair the built-in limiter with Vercel Firewall + a Spend Management cap when exposing the URL publicly.

Example Prompts

Ask your MCP client:

Use suggest_portlandmaps_address for 4521 SE Belmont St, then use get_portlandmaps_detail with the returned detail_type/detail_id and sections='list'.
Use query_intersects with include=['row','describe','taxlot'] for this Web Mercator point and explain what right-of-way data came back.

Development Notes

  • The server uses stdio transport, which is the normal local MCP integration path.
  • Responses include structuredContent when supported by the MCP client and a JSON text fallback for all clients.
  • HTTP responses are cached in memory for one hour by default.
  • Avoid broad spatial queries. Prefer address/property ID lookups and bounded geometry queries.

Publishing to the MCP Registry

This repo is set up to publish to the official MCP Registry. The registry entry is described by server.json; its name (io.github.GPTNowLLC/portlandmaps-mcp) matches the mcpName field in package.json, which is how the registry verifies npm ownership.

To cut a release (maintainers):

# 1. Bump the version in BOTH package.json and server.json (keep them in sync),
#    then publish the artifact to npm. prepublishOnly runs the build for you.
npm publish

# 2. Install the publisher CLI once (Homebrew shown; see registry docs for others)
brew install mcp-publisher

# 3. Authenticate as someone with access to the GPTNowLLC GitHub org, then publish
mcp-publisher login github
mcp-publisher publish

# 4. Verify it landed
curl "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.GPTNowLLC/portlandmaps-mcp"

The three versions — package.json, server.json (top level), and server.json's packages[0].version — must all match the version you published to npm, or the registry rejects the publish.

Important Caveat

This project gathers public data and computes rough planning inputs. It does not determine code compliance, replace Portland BDS review, or verify current ADU/tiny house rules.

Troubleshooting

Claude Desktop MCP is located here: ~/Library/Application Support/Claude/claude_desktop_config.json