friends-mcp
The friends-mcp server is a friend/identity relationship management system for AI agents — a read/write surface for tracking who an agent knows, managing trust levels, and coordinating cross-channel identities.
Identity & Friend Record Management
resolve_party— Resolve an external identity (AAD, Teams, iMessage, etc.) into a friend record, creating one on first contactget_friend— Fetch a single friend record by UUID or namelist_friends— List friends, optionally filtered by trust level (family/friend/acquaintance/stranger) and kind (human/agent)link_identity— Link an external identity to an existing friend, merging orphan/duplicate records for cross-channel unificationunlink_identity— Remove an external identity from a friend record
Trust Management
describe_trust— Explain a friend's trust context: level, basis (direct/shared_group/unknown), permitted actions, and constraintsset_trust— Assign a trust level (family, friend, acquaintance, stranger), mirrored to role
Notes & Interactions
save_note— Save a friend's name, tool preference, or general note, with optional override and provenancerecord_interaction— Log token usage or shared-mission outcomes to accumulate familiarity
Group & Multi-Party Context
upsert_group— Link participants to a shared group, automatically promoting strangers to acquaintancesresolve_room— Resolve a room into its members, each with trust context and how they are known
Agent-to-Agent (A2A)
onboard_agent— Upsert an agent-peer friend record from resolved coordinates, with optional A2A endpoint details and trust level
Self & Channel Introspection
whoami— Identify the machine owner and which friend record represents the agent itselfchannel_caps— Return a channel's capabilities (integrations, markdown, streaming, rich cards, max message length)
Profile Sharing
share_profile— Currently reserved; returns{ supported: false }pending cross-agent federation implementation
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@friends-mcpSet trust level for user john@example.com to friend"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@ouro.bot/friends
The who's-who of an agent — its identity, relationship, and trust substrate.
"It is the time you have wasted for your rose that makes your rose so important. […] People have forgotten this truth," said the fox. "But you must not forget it. You become responsible, forever, for what you have tamed." — Antoine de Saint-Exupéry, The Little Prince
friends is where an agent keeps track of who it knows. Every person and peer the agent
meets becomes a FriendRecord — a single merged identity (who they are across channels) and
the notes the agent has written about them. Relationships sit on a trust ladder, and the
agent's behavior is gated by where someone sits on it.
This is the soul of the fox's lesson: a stranger is just another voice until ties are
established. Establishing those ties — taming, in the book's word — is what moves someone up
the ladder from stranger to acquaintance to friend to family, and what makes the agent
responsible for them.
The trust ladder
Level | Meaning | Grants |
| The machine owner and those closest. | Full tool access, proactive follow-through, local operations. |
| A directly-trusted relationship. | Full collaborative access (same as family for gating purposes). |
| Known through a shared group context, not direct endorsement. | Group-safe coordination; guarded local actions. |
| Cold first contact. | Safe orientation only; no privileged actions. |
family and friend are the trusted levels (TRUSTED_LEVELS / isTrustedLevel) — they
unlock full tool access and proactive sends. acquaintance and stranger are gated.
Trust is assigned, not guessed:
First contact on a populated bundle starts at
stranger.The machine owner (the OS user running the daemon) resolves to
family— they own the agent and its bundle, so they are never a stranger.A shared group (a group chat) promotes its participants from
strangertoacquaintance— the agent now knows them through a context it trusts.
Related MCP server: AgentVeil Protocol
Multi-party and multi-agent
friends is not just 1:1. It models:
Multi-party — group chats route through
upsertGroupContextParticipants, which links every participant to the shared group and promotes strangers to acquaintances.Multi-agent — peers reached over the A2A protocol (
a2a-agentprovider) resolve tokind: "agent"records carryingAgentMeta(bundle name, familiarity, shared missions, outcomes, and A2A card/endpoint coordinates).
How it's consumed
Two seams. You bring a store; you resolve through the resolver.
import { FileFriendStore, FriendResolver, describeTrustContext } from "@ouro.bot/friends"
// 1. A store — where friend records live. FileFriendStore persists one JSON file
// per friend under the directory you give it. Or implement FriendStore yourself.
const store = new FileFriendStore("/path/to/bundle/friends")
// 2. A resolver — turns an incoming external identity into a FriendRecord +
// the capabilities of the channel it arrived on. Created per incoming message.
const { friend, channel } = await new FriendResolver(store, {
provider: "aad",
externalId: "aad-object-id",
tenantId: "tenant-guid",
displayName: "Jordan",
channel: "teams",
}).resolve()
// 3. Gate behavior on trust.
const trust = describeTrustContext({ friend, channel: channel.channel })
// → { level, basis: "direct" | "shared_group" | "unknown", permits, constraints, ... }FriendStore is the injectable abstraction — no friend code touches fs directly except the
FileFriendStore adapter, so you can back friends with anything (in-memory, a database, a
remote service) by implementing the interface.
Storage is first-class — bring your own
friends never decides where or how your data lives. Where is the path / connection
string you pass; how is a FriendStore / GrantStore implementation you choose or write. The
core domain logic — resolver, trust, notes, consent, share, import — is 100%
persistence-agnostic: it only ever calls the two store interfaces.
openFileBundle is a one-liner for the filesystem case, encapsulating the sibling _grants/
convention (the explicit two-store construction stays available):
import { openFileBundle } from "@ouro.bot/friends"
const { store, grants } = openFileBundle("/bundle/friends") // grants live at /bundle/friends/_grantsThe two seams as a contract
A third-party backend implements two interfaces. Get these three behaviors right or cross-channel / cross-agent unification breaks:
findByExternalId(provider, externalId, tenantId?)— the cross-agent join-key lookup. A match requiresprovider+externalIdand (tenantIdundefined ⇒ any tenant, else an exact tenant match). This is how the same person is recognized across channels and how an import resolves its subject by join key.get(id)— UUID-then-name fallback. Look up by UUID first; if not found, fall back to a case-insensitive name lookup (the documented path for proactive sends). A DB backend should index the UUID and MAY implement the name fallback.Round-trip discipline (load-bearing). A backend MUST preserve the full
FriendRecordlosslessly — includingimportedNotesand future additive fields (e.g.agentMeta.a2a.mailbox). Storing a lossy projection breaks the schemaVersion-1 guarantee for non-file backends. Prefer storing the whole record as a JSON blob keyed by id, with side indexes for lookups.
Sketch: a SQLite backend (illustrative — not shipped code)
The entire moat works unchanged over a database, because the domain only ever calls the
FriendStore interface. Store the record as a JSON blob (lossless) with an index table for the
join-key lookup:
// friends(id TEXT PRIMARY KEY, name TEXT, record TEXT /* JSON */)
// external_ids(provider TEXT, external_id TEXT, tenant_id TEXT, friend_id TEXT)
class SqliteFriendStore implements FriendStore {
constructor(private readonly db: Database) {}
async put(id: string, record: FriendRecord): Promise<void> {
// Lossless: the WHOLE record as JSON — importedNotes + any additive field survive.
this.db.run("INSERT OR REPLACE INTO friends (id, name, record) VALUES (?, ?, ?)",
id, record.name, JSON.stringify(record))
this.db.run("DELETE FROM external_ids WHERE friend_id = ?", id)
for (const ext of record.externalIds) {
this.db.run("INSERT INTO external_ids (provider, external_id, tenant_id, friend_id) VALUES (?, ?, ?, ?)",
ext.provider, ext.externalId, ext.tenantId ?? null, id)
}
}
async get(id: string): Promise<FriendRecord | null> {
const byId = this.db.get("SELECT record FROM friends WHERE id = ?", id)
if (byId) return JSON.parse(byId.record)
// UUID-then-name fallback (case-insensitive).
const byName = this.db.get("SELECT record FROM friends WHERE LOWER(name) = LOWER(?)", id)
return byName ? JSON.parse(byName.record) : null
}
async findByExternalId(provider: string, externalId: string, tenantId?: string): Promise<FriendRecord | null> {
const row = this.db.get(
"SELECT friend_id FROM external_ids WHERE provider = ? AND external_id = ? AND (? IS NULL OR tenant_id = ?)",
provider, externalId, tenantId ?? null, tenantId ?? null)
return row ? this.get(row.friend_id) : null
}
// delete / listAll / hasAnyFriends follow the same id-keyed-blob shape.
}GrantStore is the same shape — an id-keyed JSON blob (no external-id index needed). Swap either
store in and every import-safety invariant still holds, because they are structural
properties of the domain logic, not of the filesystem.
Channels
Each channel an agent speaks on (cli, teams, bluebubbles, mail, voice, a2a,
inner, mcp) has fixed capabilities — its sense type (open / closed / local /
internal), which integrations it exposes, and whether it supports markdown, streaming, and
rich cards. Look them up with getChannelCapabilities. The sense type, combined with trust,
is what decides whether a first-contact stranger reaches the full model on an open channel.
Observability
The package emits structured events through emitNervesEvent. By default these are dropped
(no-op) so the package is fully self-contained. To forward them somewhere real, inject an
emitter once at startup:
import { setNervesEmitter } from "@ouro.bot/friends"
setNervesEmitter((event) => {
// forward `event` to your logging / observability pipeline
})MCP server
@ouro.bot/friends ships an MCP server (friends-mcp) that exposes the library as a tool
surface for any MCP-speaking harness. The server runs no agent turn — it is a pure record
read/write surface over the library, which is exactly what makes it harness-agnostic. No
daemon, no LLM, no session: each tool call reads or writes friend records against a directory
you point it at.
Configuration (.mcp.json)
The server speaks JSON-RPC 2.0 over stdio with dual framing — Content-Length (Claude Code) and newline-delimited JSON (Codex), auto-detected from the first message.
The documented (published) form uses npx — but note it requires the package to be
published to npm first (not yet live):
{
"mcpServers": {
"friends": {
"command": "npx",
"args": ["-y", "--package", "@ouro.bot/friends", "friends-mcp", "--dir", "<path-to-friends-dir>"]
}
}
}Until then, the dev / node form is what runs against a local build:
{
"mcpServers": {
"friends": {
"command": "node",
"args": ["<repo>/dist/mcp/bin.js", "--dir", "<path-to-friends-dir>"]
}
}
}For local development you can also npm pack then
npx -y --package ./ouro.bot-friends-<version>.tgz friends-mcp --dir <path>, or npm link
then friends-mcp --dir <path>.
The --dir coupling
The store directory is the only coupling between the server and a bundle. Provide it with
--dir <path> or the FRIENDS_DIR environment variable; the flag wins when both are set,
and one of them is required (the server exits otherwise). It points at the bundle's friends/
directory — the same directory a FileFriendStore persists to.
Tool surface
19 tools, a thin 1:1 mapping over the library (no domain logic in the server):
Tool | What it does |
| Resolve an external identity into a friend record (creating one on first contact); returns |
| Explain a friend's trust context (level, basis, permits, constraints). |
| Fetch one friend record by uuid or name. |
| List friends, optionally filtered by trust / kind and limited. |
| Save a friend's name, a tool preference, or a general note (with |
| Accumulate token usage and/or append a shared-mission outcome. |
| Link participants to a shared group, promoting strangers to acquaintances. |
| Set a friend's trust level (mirrored onto |
| Link an external identity, merging any orphan record that holds it. |
| Remove an external identity from a friend. |
| Upsert an agent-peer record from resolved coordinates (no HTTP fetch). |
| Resolve the machine owner and which record represents the self. |
| Return a channel's capabilities. |
| Resolve a room (a group's external id) into its members, each with trust context and |
| Producer — prepare a consent-gated, scope-filtered, provenance-preserving profile-share envelope for another agent. |
| Consumer — import a profile-share envelope (non-clobbering merge into the imported namespace; never touches first-party notes or trust). |
| Mint an explicit, revocable consent grant (an agent may receive a scope of a friend's profile). |
| Revoke a consent grant by id (tombstones it; the right-to-be-forgotten lever). |
| List consent grants with their effective state (the audit + revoke surface). |
The share_profile / import_profile / grant_share / revoke_share / list_shares tools
need a grant store (consent persistence). The bin wires one automatically at a sibling
_grants/ directory under --dir; an embedded server gets one by passing grants to
createFriendsMcpServer. Without it those five tools report { ok: false, status: "unsupported" }
and everything else works store-only.
The server module is consumed in code from the @ouro.bot/friends/mcp subpath, exporting
createFriendsMcpServer, getToolSchemas, and runMain (plus the McpToolSchema,
FriendsMcpServer, and RunMainIo types).
The ./a2a git-mailbox transport
The package ships an optional @ouro.bot/friends/a2a sub-export — a pure git-mailbox
transport for the cross-agent moat. It has zero runtime dependencies and does no git or
network itself: the host does every git op (clone / pull / add / commit / push) and writes
the bytes; the library only computes a message file's path + bytes and parses / validates /
orders / dedups the files the host hands back.
import { buildOutgoing, readIncoming, markSeen, isSeen } from "@ouro.bot/friends/a2a"
// Producer: compute the file to write (the host then `git add/commit/push`es it).
const { relativePath, bytes } = buildOutgoing({ envelope, fromAgentId: "agent-a", toAgentId: "agent-b" })
// relativePath → agents/agent-a/outbox/agent-b/<issuedAt>--<uuid>.json
// Consumer: the host `git pull`s + reads the files, then validates/orders/dedups them.
const { ready, skippedSeen, rejected } = readIncoming({ files, selfAgentId: "agent-b", seen })
// ready: self-addressed, path-bound, not-yet-seen messages, ordered by issuedAt
// skippedSeen: messageIds already in the seen ledger (replay-safe)
// rejected: { relativePath, reason } — e.g. from_path_mismatch (a spoofed sender)Frame the two sides generically as two agents that authenticate as two distinct git
identities, sharing a dedicated private mailbox repo. Addressing lives in the path
(agents/<from>/outbox/<to>/…), each agent is the single writer of its own outbox dir, and
readIncoming path-binds every message — rejecting any whose claimed sender/recipient
doesn't match the path. The mailbox is untrusted infrastructure: a hostile mailbox can only
deny or replay, never escalate, because import_profile never touches first-party notes or
trust. See examples/a2a-git-mailbox.ts
(npm run example:a2a-git-mailbox) for an end-to-end, git-free proof of every invariant.
Cross-agent sharing (the moat)
Two different agents (different owners) can agree a party is the same person and share what
they know about them — with consent, without first-party knowledge being clobbered. The
package stays store-only and transport-agnostic: it produces and consumes a ProfileShareEnvelope;
the wire between two agents is the caller's job (the same split that keeps A2A transport
harness-side). The package does authorization — how much a verified peer's claims count, via
the trust ladder; authentication of the wire is plugged in through an AgentVerifier.
import {
prepareProfileShare, importProfileShare,
grantShare, listShares, revokeShare,
FileFriendStore, FileGrantStore, grantsDirFor,
} from "@ouro.bot/friends"
const store = new FileFriendStore("/bundle/friends")
const grants = new FileGrantStore(grantsDirFor("/bundle/friends")) // sibling _grants/ dir
// Consent is an explicit, auditable, revocable grant.
await grantShare(grants, { subjectFriendId, recipientAgentId, scope: "notes:safe" })
// Producer: a consent-gated, scope-filtered, provenance-preserving envelope that
// names the party by JOIN KEY (externalIds), never the local UUID.
const out = await prepareProfileShare(store, grants, {
friendId, toAgentId: recipientAgentId, scope: "notes:safe", selfAgentId,
})
// → { ok: true, envelope } | { ok: false, status }
// ...caller ships `out.envelope` to the other agent over its own transport...
// Consumer: the non-clobbering merge, on the OTHER agent's store.
const result = await importProfileShare(store, {
envelope, fromAgentId, trustOfSource, // this agent's resolved trust in the source
})
// → { ok: true, status: "imported" | "seeded", record } | { ok: false, status }
// Audit + revoke (the right-to-be-forgotten seam).
await listShares(grants, { subjectFriendId })
await revokeShare(grants, grantId)Import safety invariants (each is structurally enforced and tested):
the party is resolved by join key (
findByExternalIdover the envelope'sexternalIds);imported facts land in a separate
importedNotesnamespace (origin: "imported"+assertedBy+importedAt) — first-partynotesare physically untouchable; first-party always wins;the source agent's trust caps acceptance — a
strangersource is refused (the floor is configurable viaminTrustToAccept);imports NEVER change the party's trust level (non-transitive — the single most important invariant);
an unknown party is seeded (at
acquaintance) only when the introducing peer isfriend/family; astranger/acquaintancepeer may not seed a new record.
Provenance is never laundered: a first-party note shared onward is attributed to this agent;
an imported note shared onward carries its originallyAssertedBy through, so an imported fact
never masquerades as first-party.
Consent posture — the swap point
The producer is gated by a ConsentPolicy. Three postures ship, sharing one machinery, so
choosing a posture is a one-line default swap, not a rebuild:
strictPolicy— consented only by a non-revoked, non-expired explicitShareGrant.trustImpliedPolicy— an explicit grant, or recipient trust ≥friend(any scope).tieredPolicy(default) — identity-scope shares (the join key:name/identity) are consented on recipient trust ≥friend; any note-content scope (notes:*,outcomes) requires an explicit grant.
The swap point is DEFAULT_CONSENT_POLICY in src/consent.ts. Point it at
strictPolicy / trustImpliedPolicy / tieredPolicy to change the product's privacy posture
globally; or pass an explicit policy as the 4th argument to prepareProfileShare to override
per-call. The AgentVerifier defaults to trust-on-first-use (tofuVerifier), which ignores the
envelope's reserved opaque proof slot — a stronger verifier (DID/VC) can be dropped in with no
envelope change.
Public API
Types: FriendRecord, FriendConnection, ExternalId, IdentityProvider, Channel,
TrustLevel, AgentMeta, AgentAttribution, RelationshipOutcome, NoteProvenance,
ImportedNote, ShareScope, ShareGrant, ChannelCapabilities, ResolvedContext, SenseType,
Facing, TrustExplanation, TrustBasis, FriendStore, GrantStore, FriendResolverParams,
GroupContextParticipant, GroupContextUpsertResult, UsageData, FriendOpResult,
FriendOpStatus, ApplyFriendNoteInput, WhoamiResult, RoomView, RoomMember, RoomKnownVia,
ConsentPolicy, ConsentRecipient, ConsentDecisionInput, AgentVerifier,
ProfileShareEnvelope, SharedNote, PrepareProfileShareInput, PrepareProfileShareResult,
PrepareProfileShareStatus, ImportProfileShareInput, ImportProfileShareOptions,
ImportProfileShareResult, ImportProfileShareStatus, GrantShareInput, RevokeShareResult,
ListSharesFilter, ListedShare, FileBundle, NervesEvent.
Values: TRUSTED_LEVELS, IDENTITY_SCOPES, isTrustedLevel, isIdentityProvider,
isShareScope, FileFriendStore, FileGrantStore, grantsDirFor, FriendResolver,
machineOwnerUsername, isLocalMachineOwnerIdentity, getChannelCapabilities, channelToFacing,
isRemoteChannel, getAlwaysOnSenseNames, describeTrustContext,
upsertGroupContextParticipants, accumulateFriendTokens, applyFriendNote, setFriendTrust,
linkExternalId, unlinkExternalId, upsertAgentPeer, recordRelationshipOutcome, whoami,
resolveRoom, strictPolicy, trustImpliedPolicy, tieredPolicy, DEFAULT_CONSENT_POLICY,
tofuVerifier, DEFAULT_AGENT_VERIFIER, prepareProfileShare, importProfileShare, grantShare,
revokeShare, listShares, isGrantEffective, openFileBundle, setNervesEmitter.
From @ouro.bot/friends/mcp: createFriendsMcpServer, getToolSchemas, runMain.
From @ouro.bot/friends/a2a: buildOutgoing, readIncoming, markSeen, isSeen,
compareReady, MAILBOX_VERSION (+ the MailboxMessage, BuildOutgoingInput,
BuildOutgoingResult, IncomingFile, IncomingMessage, ReadIncomingInput, ReadIncomingResult,
RejectedMessage, SeenLedger types).
License
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ourostack/friends'
If you have feedback or need assistance with the MCP directory API, please join our Discord server