# Neo4j — MCP: Four Tools, One Knowledge Graph: Neo4j Governance from the Agent Side

An AI agent connected to CoreModels over MCP sees a small, deliberately shaped set of vendor-integration tools. Three of them cannot write anything at all; the fourth requires Admin membership on the project *and* a different endpoint. That shape is the point of this article: an agent can drive the entire Neo4j governance loop - discover, audit, report, generate the fix - while the only mutating step in the loop stays behind an explicit privilege boundary.

# Four Tools, One Knowledge Graph: Neo4j Governance from the Agent Side

An AI agent connected to CoreModels over MCP sees a small, deliberately shaped set of vendor-integration tools. Three of them cannot write anything at all; the fourth requires Admin membership on the project *and* a different endpoint. That shape is the point of this article: an agent can drive the entire Neo4j governance loop - discover, audit, report, generate the fix - while the only mutating step in the loop stays behind an explicit privilege boundary.

This matters more for Neo4j than for most estates. Neo4j is where a lot of teams run retrieval for their AI systems, which means the same agent that queries the graph can now check whether the graph still means what the business governed it to mean, over the same protocol.

## Connecting

CoreModels serves MCP over streamable HTTP with OAuth 2.0. The public endpoint at `/mcp` carries the read-only, Viewer-role tools; the write tools live on a separate admin endpoint. Dynamic Client Registration and PKCE are supported, so a spec-compliant client needs no pre-registered client id - point it at the URL and complete the browser flow.

From Claude Code:

```bash
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
```

From a generic JSON-configured client:

```json
{
  "mcpServers": {
    "coremodels": { "type": "http", "url": "https://coremodels.example.com/mcp" }
  }
}
```

Authorization is enforced per project and per tool. Read-only tools require Viewer membership; write tools require Editor or Admin membership regardless of which endpoint the token was minted for. Every tool also declares its read-only and open-world hints in `tools/list`, so a well-built client can show the user which calls can change something before it lets the model make them.

Two conventions apply to every integration tool below: `graphProjectId` must match `^[a-f0-9]{32}$`, and the argument schemas are closed (`additionalProperties: false`), so a hallucinated argument name is rejected by the schema rather than silently ignored.

## Call 1 - `get_vendor_integration_status`: never hard-code the contract

An agent should not assume what a connector expects; it can ask. With only a project id, the tool lists every registered connector with its capabilities and artifact notes:

```json
{ "graphProjectId": "0f3a9c81d2e6478bbf51a7c3949e02dd" }
```

The Neo4j entry that comes back declares `Import, Audit, Generate` and names its two artifacts - `meta_schema` (required, the value of `CALL apoc.meta.schema()`) and `constraints` (optional, `SHOW CONSTRAINTS` rows). Add the vendor key and the same tool answers a different question: what does this project already know about this estate?

```json
{ "graphProjectId": "0f3a9c81d2e6478bbf51a7c3949e02dd", "vendor": "neo4j" }
```

The result reports whether an import has happened, the recorded state (when it ran, the artifact fingerprint, counts, parser facts), and how many imported identities currently resolve to governed Types. This is the cheap pre-flight: `imported: false` means there is nothing to drift-check yet, and the agent should say so instead of running an audit that reports every label as unmapped.

## Call 2 - `audit_vendor_project`: the read-only workhorse

The audit compares fresh Neo4j artifacts against the governed graph and writes nothing. Viewer role, so it is available on the public endpoint:

```json
{
  "graphProjectId": "0f3a9c81d2e6478bbf51a7c3949e02dd",
  "vendor": "neo4j",
  "artifacts": {
    "meta_schema": "{\"Person\":{\"type\":\"node\",\"count\":1200,\"properties\":{\"email\":{\"type\":\"STRING\",\"unique\":true}},\"relationships\":{\"WORKS_FOR\":{\"direction\":\"out\",\"labels\":[\"Company\"]}}},\"Company\":{\"type\":\"node\",\"count\":200,\"properties\":{\"name\":{\"type\":\"STRING\"}}}}",
    "constraints": "[{\"type\":\"UNIQUENESS\",\"labelsOrTypes\":[\"Person\"],\"properties\":[\"email\"]}]"
  },
  "spaces": []
}
```

The result gives an agent everything it needs to decide what to do next:

- **`errorCount`, `warningCount`, `infoCount`** - the counters. `errorCount > 0` is the fail signal: governed meaning is violated.
- **`metrics`** - the headline measurements, including how many of the estate's datasets and fields are governed and what share of governed nodes carry a canonical (non-vendor) mapping.
- **`findings`** - each with `section` (`Coverage`, `Drift`, `Conformance`), `severity` (`Error`, `Warning`, `Info`), a stable kebab-case `code`, the `subject`, a `message`, and optional `detail`.
- **`markdown`** - the same report rendered for humans, ready to paste into a chat reply or a pull-request comment.
- **`fetchProblems`** - anything the server could not retrieve when you used URLs (see below).

The codes are the agent's decision table. `field-type-drift` in the Drift section means a governed property's type moved in the estate - escalate. `label-no-unique-id` in the Conformance section means a label has no uniqueness constraint, so `MERGE`-based ingestion can create duplicates silently - suggest a constraint, and, as we will see, generate it. `polymorphic-relationship` is informational: a relationship targets several labels and only the first is governed as a reference.

Worth knowing precisely: the MCP result is a focused subset of what the HTTP audit returns. It carries the counts, metrics, findings, and markdown; the aggregated `codes` map, `driftedObjects`, artifact `fingerprint`, and `lossiness` ledger are on the HTTP responses. An agent that needs the fingerprint to compare two runs should read the history over HTTP or count codes from `findings` itself.

## The `artifactUrls` flow

A `meta_schema` export from a production knowledge graph can be far too large to inline in a tool call. Every artifact-bearing integration tool therefore accepts a parallel argument that hands the server a URL instead:

```json
{
  "graphProjectId": "0f3a9c81d2e6478bbf51a7c3949e02dd",
  "vendor": "neo4j",
  "artifactUrls": {
    "meta_schema": "https://artifacts.example.com/exports/neo4j/2026-08-04/meta_schema.json",
    "constraints": "https://artifacts.example.com/exports/neo4j/2026-08-04/constraints.json"
  }
}
```

You can mix the two freely - small payloads inline under `artifacts`, large ones by reference under `artifactUrls` - and both are merged into one artifact set before parsing.

Because these are server-side fetches of caller-supplied URLs, they are guarded: **https only**, automatic redirects disabled (a redirect to an internal host would defeat any pre-flight check), hosts that resolve to loopback, link-local, or private ranges refused, a 60-second timeout, and a hard response-size cap. A URL that fails validation or fetching does not abort the call - the reason is recorded per artifact in `fetchProblems` and the tool proceeds with whatever it could assemble. Only when *nothing* usable arrives does the tool return an error explaining that it needs `artifacts` and/or `artifactUrls`.

This matters for agent design: an agent should always read `fetchProblems`, because "the audit ran on one of two artifacts" is a materially different result from "the audit ran on both" - dropping `constraints` means uniqueness and existence facts fall back to APOC's sampled flags.

## Call 3 - `import_vendor_project`: the one write

Import takes the same argument shape - `graphProjectId`, `vendor`, `artifacts` and/or `artifactUrls`, optional `spaces` - and writes the estate into the governed graph: labels become Types, properties become Elements, outgoing typed relationships become references, and Neo4j metadata rides along on the governed nodes. It requires the **Admin** role and is served on the admin endpoint only; the public `/mcp` endpoint does not expose it at all.

Two properties make this a tool you can let an agent run without holding your breath. First, import is additive: already-governed nodes are never mutated, so a re-run cannot destroy meaning - the tool's own description points agents at the audit to see what changed instead. Second, the outcome is fully accounted for: counts for datasets, fields, and enriched nodes, a lossiness ledger for anything approximated, and errors reserved for what genuinely could not proceed.

## Call 4 - `generate_vendor_artifacts`: hand back the fix

Generation is read-only (Viewer role, public endpoint) and turns governed meaning into Neo4j enforcement:

```json
{
  "graphProjectId": "0f3a9c81d2e6478bbf51a7c3949e02dd",
  "vendor": "neo4j",
  "typeNames": ["Person", "Company"],
  "spaces": []
}
```

`typeNames` narrows the output to named governed types; omit it to cover everything eligible. The result contains one artifact - `coremodels_constraints.cypher`, kind `cypher` - with idempotent `CREATE CONSTRAINT … IF NOT EXISTS` statements for governed uniqueness and existence checks, plus indexes for taxonomy-constrained properties. The agent that flagged `label-no-unique-id` two calls ago can produce the exact remediation script in the next call and attach it to a pull request. Meaning still changes in exactly one place: CoreModels authors it, Neo4j enforces it.

There is a fifth integration tool, `reconcile_vendor_projects` (Admin, admin endpoint), which links datasets two vendor estates govern as the same physical relation. It is relevant when a Neo4j graph is loaded from a warehouse whose tables are named identically to your labels.

## The loop, end to end

A nightly governance agent for a Neo4j-backed knowledge graph needs four calls and no database credentials:

1. `get_vendor_integration_status` - is the estate imported, and what was its last-known state?
2. `audit_vendor_project` with `artifactUrls` pointing at last night's export - did anything drift?
3. Read `fetchProblems`, then the counters. On `errorCount > 0`, escalate with the `markdown` report; on warnings, summarize the conformance findings.
4. Optionally `generate_vendor_artifacts` - attach the current constraint script, so the fix travels with the report.

## Where the boundary sits

Three of the drift-loop verbs in the HTTP surface have no MCP equivalent today: re-audit, history, and the status badge. That is deliberate rather than accidental - they are the *recording* side of the drift loop, and an agent that can silently write history entries is an agent that can quietly make a badge look green. If your agent needs the trail, have it read the history route over HTTP with a Viewer credential.

Everything else holds: every result is machine-readable, every call is role-checked, the only mutating tool sits behind Admin membership on a separate endpoint, and the audit - the call an agent will make hundreds of times - cannot write anything at all.

For the artifact extraction recipe and the HTTP equivalents of these tools, see the Neo4j quickstart in the CoreModels integration docs.
