# Public map links and account-owned Map API

Look up places and create an editable map link without an account. Connect an account only to manage saved maps or create private maps. All operations are free and cannot call paid AI or geographic services.

## Create a link without an account

Connect a Streamable HTTP MCP client to `https://drawonamap.com/mcp` with no authentication. It exposes anonymous `lookup_place` and `create_map_link` tools, a `map-link-examples` resource, and account tools that request OAuth only when called. Anonymous requests cannot read or change account-owned maps. No repository checkout, local program, API token, or sign-in is needed.

Call `create_map_link` with `features` (a GeoJSON FeatureCollection) and optional `settings` (`center`, `zoom`, `basemap`). Return the result's `url` to the user. `embed_url` opens the same map in the embedded view. The result also gives `feature_count`, `url_length`, encoded `settings`, `location_resolution`, and `warnings`.

For agents that use HTTP directly, send the same arguments as JSON to `POST https://drawonamap.com/api/v1/map-links`. This operation requires no authorization or idempotency header. Browser clients can use the endpoint through CORS without credentials. For example:

```bash
curl https://drawonamap.com/api/v1/map-links \
  -H 'Content-Type: application/json' \
  --data '{"features":{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[2.2945,48.8584]},"properties":{"label":"Meeting point"}}]}}'
```

Fetch `https://drawonamap.com/api/v1/map-links/examples.json` for checked request bodies named `point`, `line`, `polygon`, and `event`. The MCP resource returns the same examples. Each example is ready to pass as the tool's arguments or the HTTP body. They illustrate drawing data; they do not establish a real event or permitted route. Use verified coordinates from the user or a reliable source. Use `lookup_place` first for named places or addresses. Check the candidate name and address, then pass its coordinates to `create_map_link`. These tools do not calculate road routes.

The geometry and style rules in **Map contract** below apply. Public links need 1–500 features. The complete HTTP or MCP request must fit within 512 KiB. Both output links must fit within 12,000 characters. Longer links are rejected. Links above 2,048 characters produce a transport warning. Drawing coordinates use five decimal places; map centers use four. Unknown fields are rejected. Input feature IDs are not retained and produce a warning. Private visibility and account options are not accepted.

The default view fits the features, with zoom limited to 14. Supplied settings override it. Features near the date line use a conservative world-spanning view. Polar coordinates produce a warning because the basemap stops near 85 degrees. The examples are checked by the shared browser decoder and by real HTTP, MCP, and headless browser tests.

The server receives the GeoJSON to generate the link. It does not create a saved map or retain map content. HTTP map-link creation allows 60 requests per client address per minute. The single MCP endpoint allows 120 requests per client address per minute, including discovery. Place lookup also has its own limit below. A hashed address key and request counter support the limit. HTTP 429 includes `Retry-After`. Anonymous tools do not use account cookies or bearer tokens.

The map data is in the URL fragment. Anyone with the link can read it. The returned `url` opens directly for editing and export without an account. Its `?edit=1` flag applies only to anonymous hash maps and grants no saved-map permission. The browser retains that flag on reload; links made with the Share control use the normal shared view. The link is not encrypted or access-controlled. Changes create a new link and do not update earlier copies. Browser history, clipboard tools, and message services can retain the link. Opening it uses normal map tiles, site requests, and browser storage.

HTTP validation errors use `error.code`, `error.message`, and `error.details`. Field details give `path` and `message`. Invalid JSON returns 400; unsupported content type returns 415; oversized requests or links return 413; invalid fields return 422. MCP reports tool failures with `isError`. A rejected request produces no link. Use the authenticated API below for account-owned storage and later updates to the same saved map.

## Connect an agent with a URL

Add `https://drawonamap.com/mcp` as a remote MCP server in your agent. The client must support Streamable HTTP. Account operations also need OAuth. You do not need to install this repository, run a server, or create an API token.

1. Add the server URL in your agent's MCP connection settings.
2. Ask for a map. The agent looks up places, checks candidate matches, and calls `create_map_link`. Open the result and start editing. No sign-in is required.
3. If you request account storage, access to an account map, or a private map, the agent calls an account tool and receives an OAuth challenge. Sign in, review the permissions, and select **Allow access**.
4. The agent can then create, read, or update your account maps. Private maps require Pro; the agent must preserve your requested privacy.

Manage or disconnect access at `https://drawonamap.com/connect-agent`. Connections expire after 30 days. Read-and-write connections allow 100 successful writes in total. They cannot make purchases or charges. Read-only connections cannot change maps. Private maps still require Pro.

Opening `/mcp` in a browser shows an indexable connection guide. Only GET and HEAD requests that explicitly accept HTML and have no protocol or authorization headers receive the guide. MCP discovery, resources, place lookup, and link creation run without OAuth. Calls to `create_map`, `get_map`, and `update_map` pass through OAuth. Responses use `Cache-Control: no-store` to keep the representations separate. The local endpoint is `http://localhost:8788/mcp`.

## Place lookup

Use `lookup_place` with one `query`, including the city and country when known. Optional `latitude` and `longitude` bias the search; supply both. The HTTP equivalent is `GET /api/v1/places?query=Eiffel%20Tower%2C%20Paris`.

Lookup requires no account or token. It does not consume an account connection's write allowance. The limit is 30 lookups per client address per minute, shared between MCP and HTTP. A limited request returns 429 and `Retry-After`.

The tool uses Photon and OpenStreetMap data. It returns up to five candidates with `name`, `address`, `[longitude, latitude]` coordinates, an OpenStreetMap source link when available, and attribution. These are candidate matches, not proof of the user's intended place. Check each name and address; ask the user if the match is unclear. Do not invent coordinates or promise a complete list of businesses. Empty results mean no matches. A provider failure returns `place_lookup_unavailable` and must not be treated as an empty result.

Copy the selected coordinates into a Point feature and use `create_map_link` by default. Use `create_map` or `update_map` when the user requests account storage. A lookup does not save a map. Private maps require Pro; never change a requested private map to public without the user's agreement. Return the saved `edit_url` to the user. This release does not include an embedded ChatGPT map, routing, or image/PDF exports.

## Map contract

- `POST /api/v1/maps`: create an owned map. Body: `title`, `visibility`, `features`, optional `settings`.
- `GET /api/v1/maps/{id}`: read an owned map and its current revision.
- `PATCH /api/v1/maps/{id}`: change `title`, `settings`, `add_features`, or `replace_features`.

`features`, `add_features`, and `replace_features` are GeoJSON FeatureCollections. The first release supports Point, LineString, and Polygon with one closed outer ring. Multi-geometries, holes, altitude, and address strings are rejected. Coordinates are `[longitude, latitude]` in WGS84. Supply verified coordinates, not guessed locations. Maximum 500 features per collection, 2,000 vertices per feature, 5,000 drawings per map, and 512 KiB per request. Larger browser maps are outside this API version.

Properties: `label` for points, `color` and `fillColor` as six-digit hex colors, integer `width` from 1 through 50, and `fillOpacity` from 0 through 1. Unknown fields are rejected. Coordinates are stored to five decimal places; viewport center to four decimal places.

Settings: `center` as `[longitude, latitude]`, `zoom` from 0 through 22, and `basemap` as `carto`, `osm`, or `opentopo`.

Visibility defaults to `private`, which requires Pro. Set `public` explicitly to create a public map. The API cannot change visibility or share access after creation. Only the owning account can use these API operations. Browser collaborators retain their existing browser permissions.

Agent writes use the same complete-map storage path as browser saves. They retain
imported layers and image references, record a history version and account
activity, and notify an open editor after the write commits. Token quota and
idempotency records commit in the same transaction. Titles use the browser rule:
trim whitespace, reject more than 120 characters, and accept an empty title to
clear the name.

A response contains `id`, `revision`, `view_url`, `edit_url`, features, settings, omitted drawing IDs, and a data-layer indicator. Both links open the normal map page. Editing the saved original requires an authorized browser account; the link itself grants no editing permission. Features have IDs such as `drawing:0`. IDs identify positions within the returned revision, not permanent identifiers across human edits.

`replace_features` must give an existing ID for each feature. Additions and replacements preserve other drawings, imported layers, and service-area configuration. No deletion operation is exposed. Shapes that cannot be represented by this GeoJSON subset appear in `omitted_drawing_ids`. Imported data layers remain in the map and are not expanded into this response.

## Example map

```json
{
  "title": "Event access",
  "visibility": "public",
  "features": {
    "type": "FeatureCollection",
    "features": [{
      "type": "Feature",
      "geometry": {"type": "Point", "coordinates": [2.35, 48.85]},
      "properties": {"label": "Venue", "color": "#5b4bff"}
    }]
  },
  "settings": {"center": [2.35, 48.85], "zoom": 15}
}
```

Validation errors use `error.code`, `error.message`, and `error.details`. Invalid batches write nothing. Responses are private and not cached.

## Account tokens

Sign in to the website. In the browser developer console on that same origin, create a token:

```js
const response = await fetch('/api/agent-tokens', {
  method: 'POST', headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({name: 'Event assistant', scopes: 'read write', write_limit: 100, max_spend_cents: 0, expires_in_days: 30})
});
const credential = await response.json();
// Store credential.token in your agent's secret store. Do not put it in a URL.
```

Only the creation response includes the secret. The database stores a SHA-256 hash. Use `Authorization: Bearer <token>` for map API calls. API tokens cannot create or revoke other tokens.

Use `GET /api/agent-tokens` in the signed-in browser to list token IDs, permissions, usage, expiry, and revocation state. Revoke with `fetch('/api/agent-tokens/' + tokenId, {method: 'DELETE'})`. Session mutations require the same Origin. Token management has no cross-origin browser access.

Permissions are `read` or `read write`. Optional `map_id` restricts a token to an owned map and prevents new map creation. Tokens expire after 1–90 days. An account can have at most 20 active tokens. `write_limit` is a lifetime limit of 0–10,000 successful map mutations, enforced atomically with the write. Read calls remain available after this limit. Revoke and issue a new token to change its limits.

`max_spend_cents` must be zero. This API has no billing operation and cannot spend AI credits. The existing Pro requirement for private maps remains in force. Editing Service Area map content also requires its subscription. Tokens do not grant browser editing access to their holders through a link.

## Safe retries and revisions

Every POST or PATCH map request requires `Idempotency-Key`: 8–128 ASCII letters, digits, hyphens, or underscores. Generate one key per intended operation. Retry the exact operation with the same key after a lost response. The server returns the original result, including its original revision, and adds `Idempotency-Replayed: true`. A different request with the same account/key gets `409 idempotency_key_reused`. Keys persist for the lifetime of the account; do not reuse them. Deleting a map clears its stored response content but retains the key to prevent recreation by a retry. A deleted original map produces `410 map_deleted` on a create retry.

Every PATCH also requires `If-Match: <revision>` from the last read. Missing revisions get 428. Changed maps get `409 revision_conflict` with the current revision. Read again, review the changes, then submit the intended update with a new key and revision. Never retry a stale update with a newer revision without examining the map.

The retry record, map write, and token quota update share one database transaction. An error rolls them all back. A successful retry consumes no extra quota, even when the token has reached its write limit. Authorization is checked again on every retry. Revoked tokens cannot retrieve stored results.

## Result verification

Successful writes return `outcome: "saved"`, `cost_cents: 0`, and `feature_results` with each created or updated drawing ID. Read the map with GET to compare its revision and stored geometry. Write responses and retry results describe the saved operation; another authorized editor can change the map later.

Errors return `applied: false`. A validation failure rejects the whole request and reports field paths, including the feature index. There is no partial import. Geometry comes from supplied coordinates only: `location_resolution` is `provided_coordinates`. Address strings are rejected, so the caller must resolve ambiguous place names before submission.

`warnings` reports coordinate rounding and omitted content. Circles, arrows, text drawings, and curved lines remain in the map, but this version does not convert them to GeoJSON. Imported layers are retained and reported with `data_layers_not_expanded`. Open the browser map to inspect those elements.

## Optional local MCP setup

The hosted URL above is the main connection method. The local stdio adapter remains available for development and clients without remote MCP support. It uses the HTTP API and account tokens.

Install the repository dependencies with `npm ci`. Configure your MCP client to launch:

```json
{
  "mcpServers": {
    "draw-on-a-map": {
      "command": "node",
      "args": ["--import", "tsx", "/absolute/path/to/map/tools/map-agent-mcp/index.ts"],
      "cwd": "/absolute/path/to/map",
      "env": {
        "MAP_API_BASE_URL": "https://drawonamap.com",
        "MAP_API_TOKEN": "<secret from your account>"
      }
    }
  }
}
```

Use your client's secret mechanism instead of committing a token to configuration. For local development, use `http://localhost:8788`. The server rejects HTTP for remote hosts and does not follow redirects with credentials.

Tools:

- `lookup_place`: `{query, latitude?, longitude?}`.
- `create_map`: `{idempotency_key, map}` with the creation body above.
- `get_map`: `{map_id}`.
- `update_map`: `{map_id, revision, idempotency_key, changes}` with the PATCH body.

Each tool returns structured JSON and a text copy. API errors set `isError: true`. After a network error, the write outcome is unknown. Retry with the original key. The adapter never silently selects a new key or a new revision.

The machine-readable HTTP contract is at `/api/v1/openapi.json`.

## HTTP example

Save the example map as `event.json`. Load the token through your shell's secret mechanism into `MAP_API_TOKEN`.

```sh
curl --fail-with-body https://drawonamap.com/api/v1/maps \
  -H "Authorization: Bearer $MAP_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: event-example-001' \
  --data-binary @event.json
```

Read with `GET /api/v1/maps/<id>`. To change its title, send `PATCH` with `{"title":"New event title"}`, a new idempotency key, and `If-Match` set to the returned revision.

## Release and verification

Apply the two `2026-09-17` agent API migrations to Core D1 before deploying the routes. The Worker also needs the `OAUTH_KV` binding from `wrangler.jsonc`. Wrangler provisions this namespace during deployment. Keep the resulting namespace ID in the release configuration so later deployments use the same grants. The schema snapshot includes both tables. Use the normal GitHub Actions or `mise run deploy` release path. The local implementation does not change production until released.

Focused checks: `npx tsx --test tests/unit/agent-api-*.test.ts tests/unit/maps-api.test.ts`.

For the complete local workflow, run `npm run dev:status` and `npm run verify:agent-api`. The check uses local D1 and a headless browser. It creates an isolated test account, exercises MCP and browser edits, checks retry and access rules, captures screenshots, and removes its test account. Evidence is stored in `.artifacts/agent-api-verification/`.

Run `npm run verify:hosted-mcp` for the hosted connection with isolated free and Pro accounts. It looks up a real place, saves the selected coordinates, opens the editable link, and saves a browser edit. It checks that free accounts cannot create private maps. The test starts with only the MCP URL. It uses SDK discovery and registration, a headless browser approval, real OAuth tokens, map tools, refresh with narrower permissions, and disconnection. It also checks redirect validation, PKCE, and CSRF. No email is sent. Evidence is stored in new `.artifacts/hosted-mcp-verification/<plan>-<timestamp>/` directories. These are SDK and browser checks against the local Worker, not proof of installation or operation inside ChatGPT. Before submission, repeat the flow in ChatGPT against the deployed server.

The hosted server uses OAuth authorization code flow with PKCE S256. It exposes authorization-server and protected-resource metadata under `/.well-known/`. OAuth access tokens apply only to `/mcp`; personal API tokens apply to the REST API. Account sessions are never accepted as MCP credentials. All transports use the same map operation code, revision checks, and atomic write limits.

## Plugin submission

The public package is in `plugins/draw-on-a-map/`. Run `npm run plugin:package` to build its ZIP. See [the submission guide](plugins/draw-on-a-map/README.md) for reviewer credentials, domain verification, ChatGPT tests, and publication. Reviewer sign-in is disabled unless an unexpired `PLUGIN_REVIEW_LOGIN` secret is configured. It only accepts a dedicated internal sample account and still requires OAuth consent.
