@kya-os/checkpoint-mcp
v0.1.0
Published
Wire a self-hosted KYA-OS MCP server into Checkpoint: register it as a server, publish its tool catalog, and report every tool dispatch attributed to it.
Readme
@kya-os/checkpoint-mcp
Wire a self-hosted KYA-OS MCP server into Checkpoint.
Register the server, publish its tool catalog, and report every tool dispatch attributed to it, so its calls, delegations and proofs show up under that server rather than only under the project.
This is the adopter path for a service that runs @kya-os/id and @kya-os/mcp on its own infrastructure.
Hobbsidian is the reference implementation.
Install
npm install @kya-os/checkpoint-mcpUse
import { createCheckpointReporter } from '@kya-os/checkpoint-mcp';
const checkpoint = createCheckpointReporter({
apiKey: process.env.CHECKPOINT_API_KEY!,
serverDid: 'did:web:example.com',
serverUrl: 'https://example.com/api/mcp',
displayName: 'Example',
sdk: { name: 'example-mcp', version: '1.0.0' },
});
// Once on boot. Idempotent, and doubles as the liveness heartbeat.
await checkpoint.registerServer({
tools: [{ name: 'vault_read', description: 'Read a file', capability: 'vault:read' }],
});
// Per tool dispatch. Never throws.
await checkpoint.reportToolCall({
toolName: 'vault_read',
capability: 'vault:read',
action: 'permit',
reason: 'capability-granted',
agentDid: verifiedAgentDid,
principalDid: delegatingUserDid,
stages: [{ name: 'delegation_verify', verdict: 'pass', latencyMicros: 1500 }],
context: { userAgent, ipAddress, path: '/api/mcp', method: 'POST' },
});Your server DID
serverDid is not a new identifier.
It is the DID you already pin as the audience on the delegations you mint, so a credential minted for your server cannot be replayed against another one.
Registration claims that DID, and every report carries it. If the two ever diverge, attribution silently falls back to project scope rather than failing loudly, so keep them derived from one value.
The tool catalog
tools is optional, and omitting it is meaningful.
Omitted means leave the stored catalog unchanged. Passing [] means this server advertises nothing.
A boot heartbeat that does not resend its catalog must not retire everything, which is why the distinction exists.
Publishing is accretive. A tool you stop listing is marked stale, never deleted, because the catalog drives per-tool authorization on the Checkpoint side and deleting an entry would drop whatever protection an operator configured against it.
Republishing never overwrites authorization, requiredScopes or requiresDelegation.
Derive the catalog from whatever already defines your MCP surface rather than hand-maintaining a copy, so the two cannot drift:
await checkpoint.registerServer({
tools: tools.map((t) => ({
name: t.name,
description: t.description,
capability: t.capability,
})),
});Durability
reportToolCall persists to an outbox before attempting delivery, then marks the outcome.
A delivery that fails stays queued; flush() sweeps it.
The default MemoryOutbox is process-local and is lost on restart.
It exists so the package works with no configuration and so tests need no database.
For production, implement OutboxPort against a real store and keep ownership of retention and your local audit trail:
import type { OutboxPort } from '@kya-os/checkpoint-mcp';
const outbox: OutboxPort = {
async enqueue({ body, toolName }) {
const [row] = await db.insert(reports).values({ body, toolName }).returning();
return { id: row.id };
},
async markDelivered(id) {
await db.update(reports).set({ deliveredAt: new Date() }).where(eq(reports.id, id));
},
async markFailed(id, error) {
await db
.update(reports)
.set({ attempts: sql`attempts + 1`, lastError: error })
.where(eq(reports.id, id));
},
async listRetryable(limit) {
return db.select().from(reports).where(isNull(reports.deliveredAt)).limit(limit);
},
};Then sweep on a cron:
export async function GET() {
const { delivered, failed } = await checkpoint.flush(25);
return Response.json({ delivered, failed });
}Delivery guarantees
Delivery is at-least-once. A report that POSTs successfully but whose markDelivered then fails stays retryable, so a later sweep re-sends it — losing the report is the worse outcome, and Checkpoint's detections writer tolerates duplicates by design.
The reporter skips rows it is itself mid-delivery on, so a flush overlapping a live report in the same process cannot double-send. Across processes it can: two instances sweeping one shared store both see the same undelivered row. If that matters for your deployment, make listRetryable claim what it returns — a locked_until column, SELECT … FOR UPDATE SKIP LOCKED, or a queue with visibility timeouts.
Your OutboxPort should avoid throwing. Every call is guarded, so a throw cannot reach your tool handler, but it does degrade behaviour: a failed mark leaves a row to be re-sent.
Failure posture
Nothing in this package throws into your tool handler.
Registration returns { registered: false, reason }, reporting returns { delivered: false, error }, and an outbox that fails degrades to direct delivery.
A server that cannot reach Checkpoint must still serve MCP.
Why this is not in @kya-os/mcp
@kya-os/mcp is the DIF TAAWG protocol reference implementation.
A Checkpoint API key and a vendor endpoint do not belong in a standards-track package, so the protocol stays neutral and the vendor binding lives here.
See also
- Self-hosted MCP servers — the endpoint contract this package speaks, including the choice between the
log-detectionandtool.invokedingest lanes.
