---
name: nightowls-tools-connections
description: Use when connecting third-party integrations to a Night Owls swarm, registering outbound MCP servers, auditing which agents hold a capability, or building a connections admin screen.
license: MIT
metadata:
  source: https://nightowls.dev/skills/nightowls-tools-connections.md
  verified: "2026-07-26"
---

# Connections, MCP servers and capability audit

Ships in `@nightowlsdev/core@^0.21.0`, `@nightowlsdev/connectors@^2.5.0`,
`@nightowlsdev/mcp@^2.2.0`, `@nightowlsdev/storage-supabase@^2.8.0` (migrations `0025` and `0026`) and
`@nightowlsdev/runner-nextjs@^3.5.0`.

## Who holds this capability

Before revoking a dangerous tool, ask who currently has it:

```ts
const { holders, warnings } = await engine.listGrantHolders(ctx, "send_email");
// [{ slug: "editor", via: "toolNameAlias", viaGrant: "mailer" },
//  { slug: "root",   via: "delegate",      path: ["editor"] }]
```

`viaGrant` is the `skillNames` entry to delete, which is not always the skill's declared name. A
grant naming an inner tool resolves to the larger skill, so the entry is what you act on.

A grant matches by its own name or by a code skill whose inner tools include it. Counting only
exact names returns an empty list precisely when a dangerous tool was handed out inside a skill.

Published heads only, because a draft is not a grant. Render `warnings`: anything that would shrink
the answer is reported rather than dropped, since a short list here reads as contained.

## Wire the connections plane

```ts
createNextjsRunner({
  engine, auth, storage, guardDefinitionAdmin,
  connectionsPlane: {
    integrations: myIntegrationProvider,
    integrationProviders: ["slack", "linear"],
    mcpRegistry,
    mcpAllowedHosts: ["mcp.example.com", ".corp.example"],
  },
});
```

You must declare `integrationProviders`. The `IntegrationProvider` seam has no enumerator, so
listing connections has nothing to iterate. The roster is the canonical boundary and every endpoint
checks it, so an undeclared provider is uniformly absent rather than invisible but still revocable.

## What will surprise you

A row's status can read `unknown` or `static`, which are wider than `ConnectionStatus`. A probe that
throws reports `unknown`, never `active`: reporting a connection healthy because the check broke is
the one answer this route must not give.

`revoke` selects by `userId`. `accountId` is output metadata on a resolved handle and cannot address
a connection.

MCP servers are HTTP-only by database constraint. A stdio server is a command plus args and the
client executes it, so a UI-editable row would make an admin screen a remote-code-execution surface.
Keep stdio servers code-defined. A stdio upsert is rejected with that reason rather than a generic
validation error.

`validateMcpUrl` does not resolve DNS. It enforces https, an allowlisted host (exact, or a leading
dot for a domain and its subdomains, with no bare wildcards) and rejects private, loopback and
link-local literals even when an allowlist entry would admit them. Pin `mcpAllowedHosts` tightly:
it is the load-bearing control, and DNS rebinding is the residual gap.

Migration `0025` drops four dormant tables from `0001`: `mcp_servers`, `api_connections`, `tools`,
`tool_versions`. They were written by no code, but export first if you wrote to them yourself.
