@ouro.bot/friends 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/changelog.json +34 -0
- package/dist/a2a/index.d.ts +102 -0
- package/dist/a2a/index.js +198 -0
- package/dist/agent-peer.d.ts +17 -0
- package/dist/agent-peer.js +57 -0
- package/dist/channel.d.ts +11 -0
- package/dist/channel.js +132 -0
- package/dist/consent.d.ts +34 -0
- package/dist/consent.js +62 -0
- package/dist/coordination.d.ts +100 -0
- package/dist/coordination.js +255 -0
- package/dist/file-bundle.d.ts +12 -0
- package/dist/file-bundle.js +23 -0
- package/dist/grant-store-file.d.ts +16 -0
- package/dist/grant-store-file.js +136 -0
- package/dist/grant-store.d.ts +7 -0
- package/dist/grant-store.js +8 -0
- package/dist/grants.d.ts +39 -0
- package/dist/grants.js +84 -0
- package/dist/group-context.d.ts +21 -0
- package/dist/group-context.js +144 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +105 -0
- package/dist/link-identity.d.ts +14 -0
- package/dist/link-identity.js +88 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +16 -0
- package/dist/mcp/dispatch.d.ts +14 -0
- package/dist/mcp/dispatch.js +432 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +14 -0
- package/dist/mcp/run-main.d.ts +7 -0
- package/dist/mcp/run-main.js +45 -0
- package/dist/mcp/schemas.d.ts +10 -0
- package/dist/mcp/schemas.js +398 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +194 -0
- package/dist/mission-share.d.ts +94 -0
- package/dist/mission-share.js +232 -0
- package/dist/mission-store-file.d.ts +18 -0
- package/dist/mission-store-file.js +153 -0
- package/dist/mission-store.d.ts +10 -0
- package/dist/mission-store.js +9 -0
- package/dist/missions.d.ts +31 -0
- package/dist/missions.js +98 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +90 -0
- package/dist/observability.d.ts +27 -0
- package/dist/observability.js +31 -0
- package/dist/outcomes.d.ts +9 -0
- package/dist/outcomes.js +51 -0
- package/dist/resolver.d.ts +28 -0
- package/dist/resolver.js +187 -0
- package/dist/results.d.ts +8 -0
- package/dist/results.js +2 -0
- package/dist/room.d.ts +22 -0
- package/dist/room.js +40 -0
- package/dist/share.d.ts +106 -0
- package/dist/share.js +223 -0
- package/dist/standing.d.ts +83 -0
- package/dist/standing.js +111 -0
- package/dist/store-file.d.ts +21 -0
- package/dist/store-file.js +264 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +4 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +26 -0
- package/dist/trust-explanation.d.ts +16 -0
- package/dist/trust-explanation.js +74 -0
- package/dist/trust-mutation.d.ts +4 -0
- package/dist/trust-mutation.js +29 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +51 -0
- package/dist/util/cap-string.d.ts +7 -0
- package/dist/util/cap-string.js +35 -0
- package/dist/verifier.d.ts +11 -0
- package/dist/verifier.js +29 -0
- package/dist/whoami.d.ts +7 -0
- package/dist/whoami.js +39 -0
- package/package.json +68 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getToolSchemas = getToolSchemas;
|
|
4
|
+
// MCP tool schemas for the friends server.
|
|
5
|
+
//
|
|
6
|
+
// 29 tools — a thin 1:1 surface over the friends library (D7): the original 14,
|
|
7
|
+
// the cross-agent moat surface (resolve_room, import_profile, grant_share,
|
|
8
|
+
// revoke_share, list_shares; share_profile is de-stubbed in place), the brick-3
|
|
9
|
+
// mission ledger (record_mission, get_mission, list_missions, share_mission,
|
|
10
|
+
// import_mission), the brick-4 earned-standing lenses (assess_standing,
|
|
11
|
+
// explain_standing — read-only, advisory; never write trust, never on the wire),
|
|
12
|
+
// and the brick-5 coordination verbs (coordinate, import_coordination,
|
|
13
|
+
// get_coordination — negotiate WHO does a mission; advisory assignment metadata).
|
|
14
|
+
// Each schema follows JSON Schema for
|
|
15
|
+
// `inputSchema` as required by MCP. The shape mirrors the harness's McpToolSchema
|
|
16
|
+
// so the same client tooling consumes both.
|
|
17
|
+
const observability_1 = require("../observability");
|
|
18
|
+
const STRING = { type: "string" };
|
|
19
|
+
function getToolSchemas() {
|
|
20
|
+
(0, observability_1.emitNervesEvent)({
|
|
21
|
+
component: "clients",
|
|
22
|
+
event: "clients.mcp_tool_schemas",
|
|
23
|
+
message: "listed friends mcp tool schemas",
|
|
24
|
+
meta: {},
|
|
25
|
+
});
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
name: "resolve_party",
|
|
29
|
+
description: "Resolve an external identity (provider + externalId on a channel) into a friend record, creating one on first contact. Returns { friend, channel, created }.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
provider: { type: "string", description: "identity provider, e.g. aad, local, teams-conversation, imessage-handle, email-address, a2a-agent" },
|
|
34
|
+
externalId: { type: "string", description: "the external identity within the provider" },
|
|
35
|
+
displayName: { type: "string", description: "display name for the party (use 'Unknown' if not known)" },
|
|
36
|
+
channel: { type: "string", description: "the channel/sense the session belongs to, e.g. cli, teams, mcp" },
|
|
37
|
+
tenantId: { type: "string", description: "optional tenant id" },
|
|
38
|
+
},
|
|
39
|
+
required: ["provider", "externalId"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "describe_trust",
|
|
44
|
+
description: "Explain the trust context for a friend: level, basis (direct/shared_group/unknown), what it permits and constrains.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
49
|
+
channel: { type: "string", description: "the channel/sense for the explanation" },
|
|
50
|
+
isGroupChat: { type: "string", description: "set to 'true' when the conversation is a group chat" },
|
|
51
|
+
},
|
|
52
|
+
required: ["friendId", "channel"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "assess_standing",
|
|
57
|
+
description: "Assess a peer's earned standing from your FIRST-PARTY outcomes with them: a tier (proven/reliable/mixed/untested/troubled), the basis count, and the tally. Advisory only — never changes trust, never shared. Returns a Standing, or { ok:false, status:'not_found' } when the friend is missing.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
62
|
+
},
|
|
63
|
+
required: ["friendId"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "explain_standing",
|
|
68
|
+
description: "Explain a peer's earned standing in words: the tier with a human summary, why, and advisory notes that frame it as input to a MANUAL trust decision (never an instruction to change trust). Returns a StandingExplanation, or { ok:false, status:'not_found' } when the friend is missing.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
73
|
+
},
|
|
74
|
+
required: ["friendId"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "get_friend",
|
|
79
|
+
description: "Fetch a single friend record by uuid or by name.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
84
|
+
},
|
|
85
|
+
required: ["friendId"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "list_friends",
|
|
90
|
+
description: "List friend records, optionally filtered by trust level and kind, optionally limited.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
trust: { type: "string", description: "filter by trust level: family/friend/acquaintance/stranger" },
|
|
95
|
+
kind: { type: "string", description: "filter by kind: human/agent" },
|
|
96
|
+
limit: { type: "string", description: "max number of records to return" },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "save_note",
|
|
102
|
+
description: "Save a friend's name, a tool preference, or a general note. Use override='true' to overwrite an existing value.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
107
|
+
type: { type: "string", enum: ["name", "tool_preference", "note"], description: "what to save" },
|
|
108
|
+
key: { type: "string", description: "key for tool_preference or note" },
|
|
109
|
+
content: { type: "string", description: "the value to save" },
|
|
110
|
+
override: { type: "string", enum: ["true", "false"], description: "set to 'true' to overwrite an existing value" },
|
|
111
|
+
provenance: { type: "object", description: "optional attribution { assertedBy: { agentId, agentName } }" },
|
|
112
|
+
},
|
|
113
|
+
required: ["friendId", "type", "content"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "record_interaction",
|
|
118
|
+
description: "Record a turn with a friend: accumulate token usage and/or append a shared-mission outcome (bumping familiarity).",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
123
|
+
usage: { type: "object", description: "token usage; only output_tokens is counted" },
|
|
124
|
+
outcome: { type: "object", description: "shared-mission outcome { missionId, result, note? }" },
|
|
125
|
+
familiarityDelta: { type: "string", description: "how much to bump familiarity (default 1)" },
|
|
126
|
+
provenance: { type: "object", description: "optional attribution for the outcome" },
|
|
127
|
+
},
|
|
128
|
+
required: ["friendId"],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "upsert_group",
|
|
133
|
+
description: "Upsert shared-group participant context: link each participant to the group, promoting strangers to acquaintances.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
groupExternalId: { type: "string", description: "the group's external id" },
|
|
138
|
+
participants: { type: "array", description: "participants [{ provider, externalId, displayName? }]" },
|
|
139
|
+
},
|
|
140
|
+
required: ["groupExternalId", "participants"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "set_trust",
|
|
145
|
+
description: "Set a friend's trust level (also mirrors it onto the record's role).",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
150
|
+
trustLevel: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "the trust level to set" },
|
|
151
|
+
},
|
|
152
|
+
required: ["friendId", "trustLevel"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "link_identity",
|
|
157
|
+
description: "Link an external identity to a friend, merging any orphan record that already holds it (cross-channel unification).",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
162
|
+
provider: STRING,
|
|
163
|
+
externalId: STRING,
|
|
164
|
+
tenantId: { type: "string", description: "optional tenant id" },
|
|
165
|
+
},
|
|
166
|
+
required: ["friendId", "provider", "externalId"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "unlink_identity",
|
|
171
|
+
description: "Remove an external identity from a friend.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
friendId: { type: "string", description: "friend uuid or name" },
|
|
176
|
+
provider: STRING,
|
|
177
|
+
externalId: STRING,
|
|
178
|
+
},
|
|
179
|
+
required: ["friendId", "provider", "externalId"],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "onboard_agent",
|
|
184
|
+
description: "Upsert an agent-peer friend record from already-resolved coordinates (no HTTP card fetch).",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
name: { type: "string", description: "the peer agent's name" },
|
|
189
|
+
agentId: { type: "string", description: "the a2a agent id" },
|
|
190
|
+
trustLevel: { type: "string", description: "trust level (default acquaintance)" },
|
|
191
|
+
a2a: { type: "object", description: "a2a coordinates { cardUrl?, endpointUrl?, protocolVersion? }" },
|
|
192
|
+
mailbox: { type: "object", description: "optional A2A git-mailbox coords { repo, selfOutboxAgentId }" },
|
|
193
|
+
bundleName: { type: "string", description: "optional bundle name" },
|
|
194
|
+
},
|
|
195
|
+
required: ["name", "agentId"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "whoami",
|
|
200
|
+
description: "Resolve who the machine owner is and which friend record represents the self.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "channel_caps",
|
|
208
|
+
description: "Return the capabilities of a channel (integrations, markdown, streaming, rich cards, max length).",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
channel: { type: "string", description: "the channel to look up" },
|
|
213
|
+
},
|
|
214
|
+
required: ["channel"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "resolve_room",
|
|
219
|
+
description: "Resolve a room (a group's external id) into its members, each with their trust context and how they're known (direct/group_only). Pure read.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
groupExternalId: { type: "string", description: "the group's external id (e.g. group:project;+;g1)" },
|
|
224
|
+
channel: { type: "string", description: "channel lens for the trust explanation (default mcp)" },
|
|
225
|
+
},
|
|
226
|
+
required: ["groupExternalId"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "share_profile",
|
|
231
|
+
description: "Producer: prepare a consent-gated, scope-filtered, provenance-preserving profile-share envelope for another agent (names the party by join key, never the local uuid). Self identity comes from whoami. Returns { ok, envelope } or { ok:false, status }.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
friendId: { type: "string", description: "the local friend to share (uuid or name)" },
|
|
236
|
+
toAgentId: { type: "string", description: "the recipient agent's join-key agentId" },
|
|
237
|
+
scope: { type: "string", enum: ["name", "identity", "notes:safe", "notes:all", "outcomes"], description: "what to share: identity scopes carry only the join key; notes:* / outcomes require an explicit grant under the default tiered policy" },
|
|
238
|
+
proof: { type: "string", description: "optional opaque proof to stamp on the envelope (for a non-TOFU recipient verifier)" },
|
|
239
|
+
},
|
|
240
|
+
required: ["friendId", "toAgentId", "scope"],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "import_profile",
|
|
245
|
+
description: "Consumer (non-clobbering merge): import a profile-share envelope. Resolves the party by join key; lands facts in the imported namespace WITHOUT touching first-party notes; source trust caps acceptance; never changes the party's trust. Returns { ok, status, record }.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
envelope: { type: "object", description: "the ProfileShareEnvelope to import" },
|
|
250
|
+
fromAgentId: { type: "string", description: "the agent the envelope arrived from (join-key agentId)" },
|
|
251
|
+
trustOfSource: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "this agent's resolved trust in the source agent — the acceptance cap" },
|
|
252
|
+
},
|
|
253
|
+
required: ["envelope", "fromAgentId", "trustOfSource"],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "grant_share",
|
|
258
|
+
description: "Mint an explicit, revocable consent grant: an agent may receive a scope of a subject (a friend's profile, keyed by friend uuid; or a mission, keyed by its missionKey). The consent half of the moat.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
subjectKey: { type: "string", description: "whose data may be shared — a local friend uuid for a profile, or a missionKey for a mission (the legacy arg name subjectFriendId is still accepted)" },
|
|
263
|
+
recipientAgentId: { type: "string", description: "the agent that may receive it (join-key agentId)" },
|
|
264
|
+
scope: { type: "string", enum: ["name", "identity", "notes:safe", "notes:all", "outcomes", "mission"], description: "the scope consented to" },
|
|
265
|
+
expiresAt: { type: "string", description: "optional ISO expiry; absent ⇒ never expires" },
|
|
266
|
+
},
|
|
267
|
+
required: ["subjectKey", "recipientAgentId", "scope"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "revoke_share",
|
|
272
|
+
description: "Revoke a consent grant by id (tombstones it; the audit trail survives). The right-to-be-forgotten lever.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
grantId: { type: "string", description: "the grant id to revoke" },
|
|
277
|
+
},
|
|
278
|
+
required: ["grantId"],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "list_shares",
|
|
283
|
+
description: "List consent grants with their effective state, optionally filtered by subject / recipient / effectiveness. The audit + revoke surface.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
subjectKey: { type: "string", description: "filter to one subject — a friend uuid or a missionKey (the legacy arg name subjectFriendId is still accepted)" },
|
|
288
|
+
recipientAgentId: { type: "string", description: "filter to one recipient agent" },
|
|
289
|
+
effectiveOnly: { type: "string", enum: ["true", "false"], description: "set to 'true' to return only grants that currently consent" },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "record_mission",
|
|
295
|
+
description: "Upsert a shared mission (brick 3) by its cross-agent missionKey: create one when the key is unknown, else merge into the existing record. Appends first-party learnings / participants / outcomes and sets status. The mission analogue of record_interaction's outcome writer.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
missionKey: { type: "string", description: "the cross-agent join key for the mission (a ticket id / repo+PR / slugged name)" },
|
|
300
|
+
title: { type: "string", description: "human title; used only when CREATING (ignored on upsert)" },
|
|
301
|
+
status: { type: "string", enum: ["active", "succeeded", "partial", "failed", "abandoned"], description: "the mission status (first-party)" },
|
|
302
|
+
participants: { type: "array", description: "agent attributions [{ agentId?, agentName? }], merged deduped by agentId" },
|
|
303
|
+
learnings: { type: "array", description: "first-party learnings [{ key, value, shareable? }] appended to the mission" },
|
|
304
|
+
outcomes: { type: "array", description: "first-party outcomes [{ missionId, result, note? }] appended to the mission" },
|
|
305
|
+
},
|
|
306
|
+
required: ["missionKey"],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "get_mission",
|
|
311
|
+
description: "Fetch a single mission record by its local uuid id.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
missionId: { type: "string", description: "the mission's local uuid id" },
|
|
316
|
+
},
|
|
317
|
+
required: ["missionId"],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "list_missions",
|
|
322
|
+
description: "List mission records, optionally limited.",
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
limit: { type: "string", description: "max number of records to return" },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "share_mission",
|
|
332
|
+
description: "Producer: prepare a consent-gated, scope-filtered, provenance-preserving mission-share envelope for another agent (names the mission by its missionKey, never the local uuid). Self identity comes from whoami. 'mission' carries the shareable learnings; 'outcomes' carries the mission's outcomes. Returns { ok, envelope } or { ok:false, status }.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
missionId: { type: "string", description: "the local mission to share (its local uuid id)" },
|
|
337
|
+
toAgentId: { type: "string", description: "the recipient agent's join-key agentId" },
|
|
338
|
+
scope: { type: "string", enum: ["mission", "outcomes"], description: "what to share: 'mission' (title/status + shareable learnings) or 'outcomes' (the result rows); both require an explicit grant under the tiered policy" },
|
|
339
|
+
proof: { type: "string", description: "optional opaque proof to stamp on the envelope (for a non-TOFU recipient verifier)" },
|
|
340
|
+
},
|
|
341
|
+
required: ["missionId", "toAgentId", "scope"],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "import_mission",
|
|
346
|
+
description: "Consumer (non-clobbering merge): import a mission-share envelope. Resolves the mission by missionKey; lands learnings in the imported namespace WITHOUT touching first-party learnings; append-merges + dedupes outcomes; source trust caps acceptance; never recomputes status/participants. Returns { ok, status, record }.",
|
|
347
|
+
inputSchema: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
envelope: { type: "object", description: "the MissionShareEnvelope to import" },
|
|
351
|
+
fromAgentId: { type: "string", description: "the agent the envelope arrived from (join-key agentId)" },
|
|
352
|
+
trustOfSource: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "this agent's resolved trust in the source agent — the acceptance cap" },
|
|
353
|
+
},
|
|
354
|
+
required: ["envelope", "fromAgentId", "trustOfSource"],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "coordinate",
|
|
359
|
+
description: "Producer (brick 5): prepare a coordination message that negotiates WHO does a mission — one of request/offer/accept/decline/handoff, named by the mission's missionKey (never the local uuid). Consent-gated via the identity-tier 'coordinate' scope (trust ≥ friend suffices). 'accept' claims the assignment for self; 'handoff' (you must hold the assignment) proposes a new assignee whose own accept confirms it (non-transitive). Self identity comes from whoami. Returns { ok, envelope } or { ok:false, status: not_found|no_consent|not_assignee }.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
missionId: { type: "string", description: "the local mission to coordinate (its local uuid id)" },
|
|
364
|
+
toAgentId: { type: "string", description: "the recipient agent's join-key agentId" },
|
|
365
|
+
intent: { type: "string", enum: ["request", "offer", "accept", "decline", "handoff"], description: "the coordination verb: request (will you take this?) / offer (I'll take this) / accept (yes, I'm on it — sets assignee=self) / decline (no) / handoff (it's yours now — you must hold the assignment; proposes a new assignee)" },
|
|
366
|
+
note: { type: "string", description: "optional free text carried on the message + logged" },
|
|
367
|
+
proposedAssignee: { type: "object", description: "the proposed new assignee { agentId?, agentName? } — meaningful ONLY on intent=handoff" },
|
|
368
|
+
proof: { type: "string", description: "optional opaque proof to stamp on the envelope (for a non-TOFU recipient verifier)" },
|
|
369
|
+
},
|
|
370
|
+
required: ["missionId", "toAgentId", "intent"],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "import_coordination",
|
|
375
|
+
description: "Consumer (brick 5, non-clobbering merge): import a coordination message. Resolves the mission by missionKey; appends the intent to coordination.log stamped origin:imported WITHOUT touching first-party learnings/notes/status; only an 'accept' sets the assignee (last-writer-wins by issuedAt); a 'handoff' never forces an assignee onto you (only your own accept does); never recomputes status/trust/standing; source trust caps acceptance; a friend/family peer may seed an unknown mission. Returns { ok, status: logged|assigned|seeded, record } or { ok:false, status }.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
envelope: { type: "object", description: "the CoordinationEnvelope to import" },
|
|
380
|
+
fromAgentId: { type: "string", description: "the agent the envelope arrived from (join-key agentId)" },
|
|
381
|
+
trustOfSource: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "this agent's resolved trust in the source agent — the acceptance cap" },
|
|
382
|
+
},
|
|
383
|
+
required: ["envelope", "fromAgentId", "trustOfSource"],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "get_coordination",
|
|
388
|
+
description: "Read lens (brick 5): return a mission's coordination state — its current assignee (who holds it; absent ⇒ unclaimed) + the append-only log of every request/offer/accept/decline/handoff that flowed. Returns { assignee, assignedAt?, log } (the empty default { assignee: undefined, log: [] } when never coordinated), or { ok:false, status:'not_found' } when the mission is missing.",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
missionId: { type: "string", description: "the mission's local uuid id" },
|
|
393
|
+
},
|
|
394
|
+
required: ["missionId"],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FriendStore } from "../store";
|
|
2
|
+
import type { GrantStore } from "../grant-store";
|
|
3
|
+
import type { MissionStore } from "../mission-store";
|
|
4
|
+
export interface FriendsMcpServerOptions {
|
|
5
|
+
store: FriendStore;
|
|
6
|
+
/** Optional consent-grant store. When omitted, the consent/share tools
|
|
7
|
+
* (grant_share / revoke_share / list_shares / share_profile) report
|
|
8
|
+
* `unsupported`; everything else works store-only. */
|
|
9
|
+
grants?: GrantStore;
|
|
10
|
+
/** Optional mission store. When omitted, the mission ledger tools
|
|
11
|
+
* (record_mission / get_mission / list_missions / share_mission /
|
|
12
|
+
* import_mission) report `unsupported`; everything else works without it. */
|
|
13
|
+
missions?: MissionStore;
|
|
14
|
+
stdin: NodeJS.ReadableStream;
|
|
15
|
+
stdout: NodeJS.WritableStream;
|
|
16
|
+
}
|
|
17
|
+
export interface FriendsMcpServer {
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createFriendsMcpServer(options: FriendsMcpServerOptions): FriendsMcpServer;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFriendsMcpServer = createFriendsMcpServer;
|
|
4
|
+
// createFriendsMcpServer — a JSON-RPC 2.0 server over stdio for the friends
|
|
5
|
+
// library. It runs NO agent turn, NO daemon, NO LLM: every tools/call is a pure
|
|
6
|
+
// record read/write dispatched to the library. That is exactly what makes it
|
|
7
|
+
// harness-agnostic.
|
|
8
|
+
//
|
|
9
|
+
// Dual stdio framing: Content-Length (Claude Code) and newline-delimited JSON
|
|
10
|
+
// (Codex), auto-detected from the first message. The framing plumbing is ported
|
|
11
|
+
// from the harness's MCP server with all conversation/socket machinery removed.
|
|
12
|
+
const observability_1 = require("../observability");
|
|
13
|
+
const schemas_1 = require("./schemas");
|
|
14
|
+
const dispatch_1 = require("./dispatch");
|
|
15
|
+
function createFriendsMcpServer(options) {
|
|
16
|
+
const { store, grants, missions, stdin, stdout } = options;
|
|
17
|
+
let buffer = "";
|
|
18
|
+
let running = false;
|
|
19
|
+
let useContentLengthFraming = true;
|
|
20
|
+
let framingDetected = false;
|
|
21
|
+
function writeResponse(response) {
|
|
22
|
+
const body = JSON.stringify(response);
|
|
23
|
+
if (useContentLengthFraming) {
|
|
24
|
+
stdout.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
stdout.write(body + "\n");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function tryParseContentLength() {
|
|
31
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
32
|
+
/* v8 ignore next -- partial header delivery only in real I/O @preserve */
|
|
33
|
+
if (headerEnd === -1)
|
|
34
|
+
return false;
|
|
35
|
+
const headerSection = buffer.slice(0, headerEnd);
|
|
36
|
+
const contentLengthMatch = headerSection.match(/Content-Length:\s*(\d+)/i);
|
|
37
|
+
if (!contentLengthMatch) {
|
|
38
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const contentLength = parseInt(contentLengthMatch[1], 10);
|
|
42
|
+
const bodyStart = headerEnd + 4;
|
|
43
|
+
/* v8 ignore next -- partial body delivery only in real I/O @preserve */
|
|
44
|
+
if (buffer.length < bodyStart + contentLength)
|
|
45
|
+
return false;
|
|
46
|
+
const body = buffer.slice(bodyStart, bodyStart + contentLength);
|
|
47
|
+
buffer = buffer.slice(bodyStart + contentLength);
|
|
48
|
+
parseAndDispatch(body);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function tryParseNewlineDelimited() {
|
|
52
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
53
|
+
/* v8 ignore next -- partial line delivery only in real I/O @preserve */
|
|
54
|
+
if (newlineIdx === -1)
|
|
55
|
+
return false;
|
|
56
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
57
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
58
|
+
if (line.length === 0)
|
|
59
|
+
return true;
|
|
60
|
+
parseAndDispatch(line);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function parseAndDispatch(body) {
|
|
64
|
+
let request;
|
|
65
|
+
try {
|
|
66
|
+
request = JSON.parse(body);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
writeResponse({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
void handleRequest(request);
|
|
73
|
+
}
|
|
74
|
+
function handleData(chunk) {
|
|
75
|
+
buffer += chunk.toString("utf-8");
|
|
76
|
+
if (!framingDetected && buffer.length > 0) {
|
|
77
|
+
useContentLengthFraming = buffer.startsWith("Content-Length:");
|
|
78
|
+
framingDetected = true;
|
|
79
|
+
}
|
|
80
|
+
while (buffer.length > 0) {
|
|
81
|
+
const hasContentLength = buffer.startsWith("Content-Length:");
|
|
82
|
+
const parsed = hasContentLength ? tryParseContentLength() : tryParseNewlineDelimited();
|
|
83
|
+
/* v8 ignore next -- break on partial message only in real I/O @preserve */
|
|
84
|
+
if (!parsed)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function handleRequest(request) {
|
|
89
|
+
(0, observability_1.emitNervesEvent)({
|
|
90
|
+
component: "clients",
|
|
91
|
+
event: "clients.mcp_request_start",
|
|
92
|
+
message: "handling friends mcp request",
|
|
93
|
+
meta: { method: request.method },
|
|
94
|
+
});
|
|
95
|
+
if (request.id === undefined) {
|
|
96
|
+
(0, observability_1.emitNervesEvent)({
|
|
97
|
+
component: "clients",
|
|
98
|
+
event: "clients.mcp_request_end",
|
|
99
|
+
message: "handled friends mcp notification",
|
|
100
|
+
meta: { method: request.method },
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
switch (request.method) {
|
|
105
|
+
case "initialize":
|
|
106
|
+
handleInitialize(request.id);
|
|
107
|
+
break;
|
|
108
|
+
case "tools/list":
|
|
109
|
+
handleToolsList(request.id);
|
|
110
|
+
break;
|
|
111
|
+
case "tools/call":
|
|
112
|
+
await handleToolsCall(request);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
writeResponse({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
id: request.id,
|
|
118
|
+
error: { code: -32601, message: `Method not found: ${request.method}` },
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
(0, observability_1.emitNervesEvent)({
|
|
123
|
+
component: "clients",
|
|
124
|
+
event: "clients.mcp_request_end",
|
|
125
|
+
message: "completed friends mcp request",
|
|
126
|
+
meta: { method: request.method },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function handleInitialize(id) {
|
|
130
|
+
writeResponse({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
id,
|
|
133
|
+
result: {
|
|
134
|
+
protocolVersion: "2024-11-05",
|
|
135
|
+
serverInfo: { name: "friends-mcp-server", version: "0.1.0" },
|
|
136
|
+
capabilities: { tools: { listChanged: false } },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function handleToolsList(id) {
|
|
141
|
+
writeResponse({ jsonrpc: "2.0", id, result: { tools: (0, schemas_1.getToolSchemas)() } });
|
|
142
|
+
}
|
|
143
|
+
async function handleToolsCall(request) {
|
|
144
|
+
const params = request.params ?? {};
|
|
145
|
+
const toolName = params.name ?? "";
|
|
146
|
+
const toolArgs = params.arguments ?? {};
|
|
147
|
+
try {
|
|
148
|
+
const { result, isError } = await (0, dispatch_1.dispatchTool)(store, toolName, toolArgs, grants, missions);
|
|
149
|
+
writeResponse({
|
|
150
|
+
jsonrpc: "2.0",
|
|
151
|
+
id: request.id,
|
|
152
|
+
result: { content: [{ type: "text", text: JSON.stringify(result) }], isError },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
/* v8 ignore next -- defensive: non-Error throw is unreachable; tests inject Error @preserve */
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
writeResponse({
|
|
159
|
+
jsonrpc: "2.0",
|
|
160
|
+
id: request.id,
|
|
161
|
+
result: { content: [{ type: "text", text: `Error: ${message}` }], isError: true },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function onData(chunk) {
|
|
166
|
+
handleData(chunk);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
start() {
|
|
170
|
+
if (running)
|
|
171
|
+
return;
|
|
172
|
+
running = true;
|
|
173
|
+
stdin.on("data", onData);
|
|
174
|
+
(0, observability_1.emitNervesEvent)({
|
|
175
|
+
component: "clients",
|
|
176
|
+
event: "clients.mcp_server_start",
|
|
177
|
+
message: "friends mcp server started",
|
|
178
|
+
meta: {},
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
stop() {
|
|
182
|
+
if (!running)
|
|
183
|
+
return;
|
|
184
|
+
running = false;
|
|
185
|
+
stdin.removeListener("data", onData);
|
|
186
|
+
(0, observability_1.emitNervesEvent)({
|
|
187
|
+
component: "clients",
|
|
188
|
+
event: "clients.mcp_server_end",
|
|
189
|
+
message: "friends mcp server stopped",
|
|
190
|
+
meta: {},
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|