Skip to main content
ClaudeWave

MCP server for PositionGuard: area-level presence for AI assistants, read-only, no coordinates. Answers only what a member has chosen to share, and says 'unknown' otherwise.

MCP ServersOfficial Registry1 stars0 forks● TypeScriptMITUpdated today
ClaudeWave Trust Score
95/100
✓ Verified
Passed
  • ✓Open-source license (MIT)
  • ✓Actively maintained (<30d)
  • ✓Clear description
  • ✓Topics declared
  • ✓Documented (README)
Last scanned: 10/16/2026
Install in Claude Code / Claude Desktop
Method: Manual
Claude Code CLI
git clone https://github.com/positionguard/positionguard-mcp
claude_desktop_config.json (Claude Desktop)
{
  "mcpServers": {
    "positionguard-mcp": {
      "command": "node",
      "args": ["/path/to/positionguard-mcp/dist/index.js"]
    }
  }
}
1. Run the command above in your terminal (Claude Code), or paste the JSON config into claude_desktop_config.json (Claude Desktop).
2. Replace any <placeholder> values with your API keys or paths.
3. Restart Claude. The MCP server and its tools appear automatically.
💡 Clone https://github.com/positionguard/positionguard-mcp and follow its README for install instructions.
Use cases

MCP Servers overview

# positionguard-mcp

An MCP server that lets an AI assistant ask about [PositionGuard](https://positionguardai.com)
presence: *is Earl at the skatepark*, *who is at home*, *how many people are
at the club*. It answers only what each member has agreed to share with
assistants, and says **unknown** when they haven't.

The server is a deterministic client of the public REST API. It holds one
API key, calls three read-only endpoints, and maps what comes back into a
small, explicit vocabulary. There is no model inside it and no privileged
access: every guarantee is enforced at `api.positionguardai.com`, where the
key is checked and the consent gate runs. This server cannot widen what the
API returns. It can only ask.

## What it asks for, and what it doesn't

This server gives the assistant less than it could, on purpose, and you can
read exactly what.

- **Three scopes, not four.** Grant `presence:read`, `counts:read` and
  `groups:read`. Do **not** grant `areas:read`: it unlocks
  `/groups/{id}/areas`, which returns area center coordinates, and this
  server never calls it. Area names and IDs come from `/area-counts`, which
  is coordinate-free by design. The client refuses to fetch `/areas` even
  if a future edit asked it to, and a test checks the refusal.
- **An Assistant key, not an Integration key.** Create the key in the
  developer portal with type **Assistant**. Members are visible to an
  Assistant key only if they have turned on *Let AI agents see whether I'm
  at an area*; otherwise the key gets *unknown*. An Integration key works
  mechanically, and bypasses that consent gate. That is the key holder's
  choice to make; the reference configuration does not demonstrate it.
- **Never log a body, a key, or a name.** Log lines carry paths, HTTP
  statuses, item counts, durations, tool names and the `status` a tool
  returned. The key appears once at startup, as its first 16 characters,
  the same prefix the portal shows.
- **`unknown` is not "away".** A member who has not opted in, who is in
  global Ghost, who paused sharing, or whose phone has not reported in a
  while all look identical on the wire, and all map to `unknown`; the
  first three are captured in `test/fixtures/` and asserted identical.
  The same holds per area: a member who is at an area but has hidden that
  one area from assistants is simply not listed there, and that absence
  is `unknown` too, never "not there". A
  member who joined a public group in Ghost mode is not on that roster at
  all, and one who joined anonymously is listed as "Anonymous"; the API
  does that before this server sees the roster. The tool descriptions
  tell the model, in so many words, that `unknown` means it doesn't know.
  A stale last-known position is never returned as a current location:
  it is `unknown`, or `last_known_at_area` with its age (below); counts
  include stale members and say so.
- **No caching.** The API caches for 15 seconds. A second cache would be a
  second place for a withdrawn consent to lag.
- **Read-only.** No write tools, no coordinates, no messages, no history.
  Every tool is annotated `readOnlyHint`.

## Tools

Every answer is JSON with a `status` field. `ok` means the question was
answered. `unknown` means it was not, and `reason` says why in one word.
API failures are MCP tool errors carrying the HTTP status in one sentence,
and are never retried.

| Tool | Scope | Answers |
|---|---|---|
| `list_groups` | `groups:read` | `{status, groups: [{group_id, name, group_type}]}` |
| `list_areas` | `counts:read` | `{status, areas: [{area_id, name, group_id, group_name}]}` — names only, no counts, no geometry |
| `where_is_member` | `presence:read` | `{status: "at_area", area, inside_areas?, since?}` \| `{status: "last_known_at_area", area, inside_areas?, since?, last_confirmed_seconds_ago, note}` \| `{status: "not_at_area"}` \| `{status: "unknown", reason}` |
| `who_is_at_area` | `presence:read` | `{status, area, confirmed: [nickname…], last_known: [{nickname, last_confirmed_seconds_ago}…], members, last_known_note?, undisclosed_note?, count_note?}` — disclosed members only |
| `count_members_at_area` | `counts:read` | `{status, member_count, stale_count, undisclosed_count, note}` |

`where_is_member` looks a nickname up case-insensitively, in one group or
across all of them. Its `reason` is `not_disclosed`, `stale`, or
`no_such_member` (which is an answer, not an error, so the model doesn't
invent one). `who_is_at_area` adds `count_note` ("N counted at this area,
M listed") whenever the server counts more members at the area than the
answer lists, where N is `member_count + undisclosed_count`: a public
group serves no row for a Ghost-joined member and, above its member
limit, no row but the caller's own, and a member can be present but not
disclosed to assistants at that area, so the list is never presented as
complete when the count says otherwise. When members are undisclosed the
note says how many, and "do not report the named list as complete".
`member_count` is a floor when `undisclosed_count` or `stale_count` is
non-zero, and the `note` says to report it as "at least N". When the API
provides no count at all (archived areas), the answer is `unknown` with
reason `count_unavailable`, never zero.

**Areas inside areas.** A group can have a home inside a city. The API
lists every area of the group a member is inside, innermost first
(`inside_areas`), and `where_is_member` answers with the innermost as
`area` and the whole list beside it, so "is Fred in Los Angeles" can be
answered from the list. `who_is_at_area` matches the area asked about
against that list, so someone at home is also named for the city.
Members choose per area what assistants may see: one who shares Home and
not Los Angeles comes back with a list of Home alone, and nothing marks
the omission. **An area missing from `inside_areas` is therefore not
evidence the member is outside it** — the tool description says so — and
the only trace of the withholding is the city's `undisclosed_count`,
which is why `count_note` reads it. `since` is given only when the area
named is the one the API timed the entry for. Against a server that
doesn't send `inside_areas`, the single `current_area` answers as before.

**Freshness and last-known answers.** A PositionGuard server can hold a
member at the area they were last confirmed in while their phone is
silent, and marks those rows `position_fresh: false` with the position's
age. `where_is_member` answers them as `last_known_at_area`, with
`last_confirmed_seconds_ago` and a note ("Last confirmed at Lake House
10 h ago; no newer position"), never as `at_area`. `who_is_at_area`
lists them under `last_known`, never under `confirmed`, and
`count_members_at_area`'s note says how many of the count are not recently
confirmed. The tool descriptions tell the model to relay a last-known
answer with its age, never as current presence. `since` is always when
the member entered the area, not when they were last confirmed. Against
a server that doesn't send `position_fresh`, answers are what they were in
0.1.0.

How the wire maps to `status` is written out in `src/core/mapping.ts` and
tested against captured responses in `test/fixtures/` before any model
sees the server.

## Configuration

| Env | Required | Default |
|---|---|---|
| `POSITIONGUARD_API_KEY` | yes | — must start with `pg_live_`; the server refuses to start otherwise |
| `POSITIONGUARD_API_BASE` | no | `https://api.positionguardai.com/api/v1` |
| `TRANSPORT` | no | `http` (streamable HTTP + SSE) or `stdio` |
| `PORT` | no | `8788` (HTTP transports) |
| `HOST` | no | `0.0.0.0` (HTTP transports); use `127.0.0.1` outside a container |

On startup the server calls `GET /groups` once. A 401 or 403 exits with a
message naming the fix. An empty group list starts with a warning. Every
tool call re-checks the key, so a briefly unreachable API at startup is
logged and not fatal.

## Transports

One code path, three ways in. There is **no authentication** on the HTTP
transports in this version: run the server on a private docker network or
on localhost, and do not expose it.

| Path | Transport | For |
|---|---|---|
| `/mcp` | Streamable HTTP, stateless | Open WebUI |
| `/sse` (+ `/messages`) | SSE | Home Assistant's `mcp` integration, which is SSE-only |
| stdio (`TRANSPORT=stdio`) | stdio | Claude Desktop and other launchers |
| `/healthz` | — | docker healthcheck; returns `{"ok":true}` |

## Setups

Each setup is marked **tested** or **untested**. Only tested setups describe
behaviour that has actually been observed with this server.

### 1. Open WebUI + Anthropic + this server, in containers — tested

`docker-compose.example.yml` runs Open WebUI and this server on one private
network with no port published for the server. Put `WEBUI_SECRET_KEY` and
`POSITIONGUARD_API_KEY` in a `.env` file (gitignored), then:

```
cp docker-compose.example.yml docker-compose.yml
docker compose up -d
```

In Open WebUI: **Admin → Settings → Connections**, add an OpenAI-compatible
connection with base URL `https://api.anthropic.com/v1` and your Anthropic
key. **Admin → Settings → External Tools**, add an MCP server of type
Streamable HTTP at `http://positionguard-mcp:8788/mcp` with auth **None**
(Bearer with an empty key sends an empty header, which servers reject).
Open WebUI has supported Streamable HTTP MCP servers natively since 0.6.31.

Disclosure for this and any other cloud-model setup: presence you've chosen
to disclose is sent to the model provider to produce answers.

Verified against the production API with an Assistant key. 
A member who has turned the setting on returns their area; the same member with it off returns unknown, not "away". 
A count with an undisclosed or stale member is reported as a floor.

### 2. Claude Desktop, stdio — tested

Build once (`npm install && npm run build`), then add to Claude Desktop's
`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "positionguard"
aiai-agentai-agentsai-governanceai-safetyai-securitylocation-basedlocation-based-serviceslocation-serviceslocation-trackingmcpmcp-clientmcp-serverpositionguardpresencepresence-detectionprivacyprivacy-firstprivacy-protectionprivacy-tools

What people ask about positionguard-mcp

What is positionguard/positionguard-mcp?

+

positionguard/positionguard-mcp is mcp servers for the Claude AI ecosystem. MCP server for PositionGuard: area-level presence for AI assistants, read-only, no coordinates. Answers only what a member has chosen to share, and says 'unknown' otherwise. It has 1 GitHub stars and its last recorded update is dated 2026-10-15.

How do I install positionguard-mcp?

+

You can install positionguard-mcp by cloning the repository (https://github.com/positionguard/positionguard-mcp) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.

Is positionguard/positionguard-mcp safe to use?

+

Our security agent has analyzed positionguard/positionguard-mcp and assigned a Trust Score of 95/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.

Who maintains positionguard/positionguard-mcp?

+

positionguard/positionguard-mcp is maintained by positionguard. The last recorded GitHub activity is dated 2026-10-15, with 0 open issues.

Are there alternatives to positionguard-mcp?

+

Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.

Deploy positionguard-mcp to your cloud

Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.

Maintain this repo? Add a badge to your README

Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.

Featured on ClaudeWave: positionguard/positionguard-mcp
[![Featured on ClaudeWave](https://claudewave.com/api/badge/positionguard-positionguard-mcp)](https://claudewave.com/repo/positionguard-positionguard-mcp)
<a href="https://claudewave.com/repo/positionguard-positionguard-mcp"><img src="https://claudewave.com/api/badge/positionguard-positionguard-mcp" alt="Featured on ClaudeWave: positionguard/positionguard-mcp" width="320" height="64" /></a>

More MCP Servers

positionguard-mcp alternatives