@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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +514 -0
  3. package/changelog.json +34 -0
  4. package/dist/a2a/index.d.ts +102 -0
  5. package/dist/a2a/index.js +198 -0
  6. package/dist/agent-peer.d.ts +17 -0
  7. package/dist/agent-peer.js +57 -0
  8. package/dist/channel.d.ts +11 -0
  9. package/dist/channel.js +132 -0
  10. package/dist/consent.d.ts +34 -0
  11. package/dist/consent.js +62 -0
  12. package/dist/coordination.d.ts +100 -0
  13. package/dist/coordination.js +255 -0
  14. package/dist/file-bundle.d.ts +12 -0
  15. package/dist/file-bundle.js +23 -0
  16. package/dist/grant-store-file.d.ts +16 -0
  17. package/dist/grant-store-file.js +136 -0
  18. package/dist/grant-store.d.ts +7 -0
  19. package/dist/grant-store.js +8 -0
  20. package/dist/grants.d.ts +39 -0
  21. package/dist/grants.js +84 -0
  22. package/dist/group-context.d.ts +21 -0
  23. package/dist/group-context.js +144 -0
  24. package/dist/index.d.ts +49 -0
  25. package/dist/index.js +105 -0
  26. package/dist/link-identity.d.ts +14 -0
  27. package/dist/link-identity.js +88 -0
  28. package/dist/mcp/bin.d.ts +2 -0
  29. package/dist/mcp/bin.js +16 -0
  30. package/dist/mcp/dispatch.d.ts +14 -0
  31. package/dist/mcp/dispatch.js +432 -0
  32. package/dist/mcp/index.d.ts +6 -0
  33. package/dist/mcp/index.js +14 -0
  34. package/dist/mcp/run-main.d.ts +7 -0
  35. package/dist/mcp/run-main.js +45 -0
  36. package/dist/mcp/schemas.d.ts +10 -0
  37. package/dist/mcp/schemas.js +398 -0
  38. package/dist/mcp/server.d.ts +21 -0
  39. package/dist/mcp/server.js +194 -0
  40. package/dist/mission-share.d.ts +94 -0
  41. package/dist/mission-share.js +232 -0
  42. package/dist/mission-store-file.d.ts +18 -0
  43. package/dist/mission-store-file.js +153 -0
  44. package/dist/mission-store.d.ts +10 -0
  45. package/dist/mission-store.js +9 -0
  46. package/dist/missions.d.ts +31 -0
  47. package/dist/missions.js +98 -0
  48. package/dist/notes.d.ts +11 -0
  49. package/dist/notes.js +90 -0
  50. package/dist/observability.d.ts +27 -0
  51. package/dist/observability.js +31 -0
  52. package/dist/outcomes.d.ts +9 -0
  53. package/dist/outcomes.js +51 -0
  54. package/dist/resolver.d.ts +28 -0
  55. package/dist/resolver.js +187 -0
  56. package/dist/results.d.ts +8 -0
  57. package/dist/results.js +2 -0
  58. package/dist/room.d.ts +22 -0
  59. package/dist/room.js +40 -0
  60. package/dist/share.d.ts +106 -0
  61. package/dist/share.js +223 -0
  62. package/dist/standing.d.ts +83 -0
  63. package/dist/standing.js +111 -0
  64. package/dist/store-file.d.ts +21 -0
  65. package/dist/store-file.js +264 -0
  66. package/dist/store.d.ts +9 -0
  67. package/dist/store.js +4 -0
  68. package/dist/tokens.d.ts +8 -0
  69. package/dist/tokens.js +26 -0
  70. package/dist/trust-explanation.d.ts +16 -0
  71. package/dist/trust-explanation.js +74 -0
  72. package/dist/trust-mutation.d.ts +4 -0
  73. package/dist/trust-mutation.js +29 -0
  74. package/dist/types.d.ts +164 -0
  75. package/dist/types.js +51 -0
  76. package/dist/util/cap-string.d.ts +7 -0
  77. package/dist/util/cap-string.js +35 -0
  78. package/dist/verifier.d.ts +11 -0
  79. package/dist/verifier.js +29 -0
  80. package/dist/whoami.d.ts +7 -0
  81. package/dist/whoami.js +39 -0
  82. package/package.json +68 -0
@@ -0,0 +1,432 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.coerceBool = coerceBool;
4
+ exports.coerceInt = coerceInt;
5
+ exports.coerceString = coerceString;
6
+ exports.coerceOptionalString = coerceOptionalString;
7
+ exports.dispatchTool = dispatchTool;
8
+ // Tool dispatch for the friends MCP server.
9
+ //
10
+ // MCP sends string-ish args; the coercion helpers normalize them before calling
11
+ // the library fns. `dispatchTool` is a flat tool → library-fn map (D9/D10) with
12
+ // NO domain logic of its own — every behavior lives in the friends library.
13
+ const observability_1 = require("../observability");
14
+ const types_1 = require("../types");
15
+ const resolver_1 = require("../resolver");
16
+ const trust_explanation_1 = require("../trust-explanation");
17
+ const standing_1 = require("../standing");
18
+ const channel_1 = require("../channel");
19
+ const group_context_1 = require("../group-context");
20
+ const tokens_1 = require("../tokens");
21
+ const notes_1 = require("../notes");
22
+ const trust_mutation_1 = require("../trust-mutation");
23
+ const link_identity_1 = require("../link-identity");
24
+ const agent_peer_1 = require("../agent-peer");
25
+ const outcomes_1 = require("../outcomes");
26
+ const whoami_1 = require("../whoami");
27
+ const room_1 = require("../room");
28
+ const share_1 = require("../share");
29
+ const grants_1 = require("../grants");
30
+ const missions_1 = require("../missions");
31
+ const mission_share_1 = require("../mission-share");
32
+ const coordination_1 = require("../coordination");
33
+ const types_2 = require("../types");
34
+ function coerceBool(v) {
35
+ return v === true || v === "true";
36
+ }
37
+ function coerceInt(v) {
38
+ if (v === undefined || v === null)
39
+ return undefined;
40
+ const n = typeof v === "number" ? v : parseInt(String(v), 10);
41
+ return Number.isNaN(n) ? undefined : n;
42
+ }
43
+ function coerceString(v) {
44
+ return typeof v === "string" ? v : "";
45
+ }
46
+ function coerceOptionalString(v) {
47
+ return typeof v === "string" ? v : undefined;
48
+ }
49
+ /** Parse a value that may be a JSON-string-encoded object/array. Returns
50
+ * undefined when the value is absent or the string fails to parse (guarded). */
51
+ function parseMaybeJson(v) {
52
+ if (v === undefined || v === null)
53
+ return undefined;
54
+ if (typeof v === "string") {
55
+ try {
56
+ return JSON.parse(v);
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ return v;
63
+ }
64
+ /** A grant-backed tool was called without a GrantStore wired (a store-only
65
+ * embedding). The consent surface needs grant persistence, so report it cleanly
66
+ * rather than guessing. */
67
+ const NO_GRANT_STORE = { ok: false, status: "unsupported", message: "no grant store configured (consent/share tools require one)" };
68
+ /** A mission-backed tool was called without a MissionStore wired (a store-only
69
+ * embedding). The mission ledger needs mission persistence, so report it cleanly
70
+ * rather than guessing. */
71
+ const NO_MISSION_STORE = { ok: false, status: "unsupported", message: "no mission store configured (mission tools require one)" };
72
+ async function dispatchTool(store, name, args, grants, missions) {
73
+ (0, observability_1.emitNervesEvent)({
74
+ component: "clients",
75
+ event: "clients.mcp_dispatch",
76
+ message: "dispatching friends mcp tool",
77
+ meta: { tool: name },
78
+ });
79
+ switch (name) {
80
+ case "resolve_party": {
81
+ const provider = coerceString(args.provider);
82
+ const externalId = coerceString(args.externalId);
83
+ const tenantId = coerceOptionalString(args.tenantId);
84
+ const displayName = coerceString(args.displayName) || "Unknown";
85
+ const channel = coerceString(args.channel);
86
+ const existing = await store.findByExternalId(provider, externalId, tenantId);
87
+ const created = existing === null;
88
+ const resolved = await new resolver_1.FriendResolver(store, { provider, externalId, tenantId, displayName, channel }).resolve();
89
+ return { result: { friend: resolved.friend, channel: resolved.channel, created }, isError: false };
90
+ }
91
+ case "describe_trust": {
92
+ const friend = await store.get(coerceString(args.friendId));
93
+ if (!friend) {
94
+ return { result: { ok: false, status: "not_found", message: "friend record not found" }, isError: true };
95
+ }
96
+ const explanation = (0, trust_explanation_1.describeTrustContext)({
97
+ friend,
98
+ channel: coerceString(args.channel),
99
+ isGroupChat: coerceBool(args.isGroupChat),
100
+ });
101
+ return { result: explanation, isError: false };
102
+ }
103
+ case "assess_standing": {
104
+ // Pure read mirroring describe_trust: resolve the record, run the store-free
105
+ // assessment, return the value. No rule injected at the boundary — the tool
106
+ // uses DEFAULT_STANDING_RULE via the fn's `rule ?? DEFAULT` fallback. Never
107
+ // writes trust; never produces a wire artifact.
108
+ const friend = await store.get(coerceString(args.friendId));
109
+ if (!friend) {
110
+ return { result: { ok: false, status: "not_found", message: "friend record not found" }, isError: true };
111
+ }
112
+ return { result: (0, standing_1.assessStanding)(friend), isError: false };
113
+ }
114
+ case "explain_standing": {
115
+ const friend = await store.get(coerceString(args.friendId));
116
+ if (!friend) {
117
+ return { result: { ok: false, status: "not_found", message: "friend record not found" }, isError: true };
118
+ }
119
+ return { result: (0, standing_1.explainStanding)(friend), isError: false };
120
+ }
121
+ case "get_friend": {
122
+ const friend = await store.get(coerceString(args.friendId));
123
+ if (!friend) {
124
+ return { result: { ok: false, status: "not_found", message: "friend record not found" }, isError: true };
125
+ }
126
+ return { result: friend, isError: false };
127
+ }
128
+ case "list_friends": {
129
+ const all = typeof store.listAll === "function" ? await store.listAll() : [];
130
+ const trust = coerceOptionalString(args.trust);
131
+ const kind = coerceOptionalString(args.kind);
132
+ const limit = coerceInt(args.limit);
133
+ let filtered = all;
134
+ if (trust !== undefined)
135
+ filtered = filtered.filter((f) => f.trustLevel === trust);
136
+ if (kind !== undefined)
137
+ filtered = filtered.filter((f) => f.kind === kind);
138
+ if (limit !== undefined)
139
+ filtered = filtered.slice(0, limit);
140
+ return { result: filtered, isError: false };
141
+ }
142
+ case "save_note": {
143
+ const type = coerceString(args.type);
144
+ // The library helper takes a typed `type` union; the MCP boundary validates
145
+ // the raw string here so an unknown type is a clean `invalid` result
146
+ // rather than garbage written under an undefined key.
147
+ if (type !== "name" && type !== "tool_preference" && type !== "note") {
148
+ return {
149
+ result: { ok: false, status: "invalid", message: `unrecognized note type '${type}' — use name, tool_preference, or note` },
150
+ isError: true,
151
+ };
152
+ }
153
+ const result = await (0, notes_1.applyFriendNote)(store, coerceString(args.friendId), {
154
+ type,
155
+ key: coerceOptionalString(args.key),
156
+ content: coerceString(args.content),
157
+ override: coerceBool(args.override),
158
+ provenance: parseMaybeJson(args.provenance),
159
+ });
160
+ return { result, isError: result.ok === false };
161
+ }
162
+ case "record_interaction": {
163
+ const friendId = coerceString(args.friendId);
164
+ const usage = parseMaybeJson(args.usage);
165
+ const outcome = parseMaybeJson(args.outcome);
166
+ const familiarityDelta = coerceInt(args.familiarityDelta);
167
+ const provenance = parseMaybeJson(args.provenance);
168
+ if (usage === undefined && outcome === undefined) {
169
+ return { result: { ok: true, status: "noop", message: "no usage or outcome provided" }, isError: false };
170
+ }
171
+ const combined = {};
172
+ if (usage !== undefined) {
173
+ await (0, tokens_1.accumulateFriendTokens)(store, friendId, usage);
174
+ combined.tokensAccumulated = true;
175
+ }
176
+ if (outcome !== undefined) {
177
+ combined.outcome = await (0, outcomes_1.recordRelationshipOutcome)(store, friendId, { missionId: outcome.missionId, result: outcome.result, note: outcome.note, provenance: outcome.provenance ?? provenance }, familiarityDelta);
178
+ }
179
+ return { result: combined, isError: false };
180
+ }
181
+ case "upsert_group": {
182
+ const participants = parseMaybeJson(args.participants) ?? [];
183
+ const results = await (0, group_context_1.upsertGroupContextParticipants)({
184
+ store,
185
+ participants,
186
+ groupExternalId: coerceString(args.groupExternalId),
187
+ });
188
+ return { result: results, isError: false };
189
+ }
190
+ case "set_trust": {
191
+ const result = await (0, trust_mutation_1.setFriendTrust)(store, coerceString(args.friendId), coerceString(args.trustLevel));
192
+ return { result, isError: result.ok === false };
193
+ }
194
+ case "link_identity": {
195
+ const result = await (0, link_identity_1.linkExternalId)(store, coerceString(args.friendId), {
196
+ provider: coerceString(args.provider),
197
+ externalId: coerceString(args.externalId),
198
+ tenantId: coerceOptionalString(args.tenantId),
199
+ });
200
+ return { result, isError: result.ok === false };
201
+ }
202
+ case "unlink_identity": {
203
+ const result = await (0, link_identity_1.unlinkExternalId)(store, coerceString(args.friendId), {
204
+ provider: coerceString(args.provider),
205
+ externalId: coerceString(args.externalId),
206
+ });
207
+ return { result, isError: result.ok === false };
208
+ }
209
+ case "onboard_agent": {
210
+ const record = await (0, agent_peer_1.upsertAgentPeer)(store, {
211
+ name: coerceString(args.name),
212
+ agentId: coerceString(args.agentId),
213
+ trustLevel: coerceOptionalString(args.trustLevel),
214
+ a2a: parseMaybeJson(args.a2a),
215
+ mailbox: parseMaybeJson(args.mailbox),
216
+ bundleName: coerceOptionalString(args.bundleName),
217
+ });
218
+ return { result: record, isError: false };
219
+ }
220
+ case "whoami": {
221
+ return { result: await (0, whoami_1.whoami)(store), isError: false };
222
+ }
223
+ case "channel_caps": {
224
+ return { result: (0, channel_1.getChannelCapabilities)(coerceString(args.channel)), isError: false };
225
+ }
226
+ case "resolve_room": {
227
+ const view = await (0, room_1.resolveRoom)(store, coerceString(args.groupExternalId), (coerceOptionalString(args.channel) ?? "mcp"));
228
+ return { result: view, isError: false };
229
+ }
230
+ case "share_profile": {
231
+ // De-stubbed producer. Self identity comes from whoami (the dispatch is
232
+ // store-only); the subject is named by join key inside the library.
233
+ if (!grants)
234
+ return { result: NO_GRANT_STORE, isError: true };
235
+ const scope = coerceString(args.scope);
236
+ if (!(0, types_1.isShareScope)(scope)) {
237
+ return {
238
+ result: { ok: false, status: "invalid", message: `unrecognized scope '${scope}' — use name, identity, notes:safe, notes:all, or outcomes` },
239
+ isError: true,
240
+ };
241
+ }
242
+ const self = await (0, whoami_1.whoami)(store);
243
+ // whoami sets selfFriendId + selfAgentName together or neither; the local
244
+ // self id (selfFriendId) is the asserter tag. "" when there is no self yet.
245
+ const selfAgentId = self.selfFriendId ?? "";
246
+ const result = await (0, share_1.prepareProfileShare)(store, grants, {
247
+ friendId: coerceString(args.friendId),
248
+ toAgentId: coerceString(args.toAgentId),
249
+ scope: scope,
250
+ selfAgentId,
251
+ proof: coerceOptionalString(args.proof),
252
+ });
253
+ return { result, isError: result.ok === false };
254
+ }
255
+ case "import_profile": {
256
+ const envelope = parseMaybeJson(args.envelope);
257
+ if (!envelope || typeof envelope !== "object") {
258
+ return { result: { ok: false, status: "invalid", message: "an envelope object is required" }, isError: true };
259
+ }
260
+ const result = await (0, share_1.importProfileShare)(store, {
261
+ envelope,
262
+ fromAgentId: coerceString(args.fromAgentId),
263
+ trustOfSource: coerceString(args.trustOfSource),
264
+ });
265
+ return { result, isError: result.ok === false };
266
+ }
267
+ case "grant_share": {
268
+ if (!grants)
269
+ return { result: NO_GRANT_STORE, isError: true };
270
+ const scope = coerceString(args.scope);
271
+ if (!(0, types_1.isShareScope)(scope)) {
272
+ return {
273
+ result: { ok: false, status: "invalid", message: `unrecognized scope '${scope}' — use name, identity, notes:safe, notes:all, or outcomes` },
274
+ isError: true,
275
+ };
276
+ }
277
+ // Fork D compat seam (b): accept the new `subjectKey` arg, falling back to
278
+ // the legacy `subjectFriendId` so old-arg callers (incl. the unmodified
279
+ // examples) keep working. coerceString gives "" when an arg is absent, so
280
+ // `||` selects subjectKey when present, else subjectFriendId.
281
+ const grant = await (0, grants_1.grantShare)(grants, {
282
+ subjectKey: coerceString(args.subjectKey) || coerceString(args.subjectFriendId),
283
+ recipientAgentId: coerceString(args.recipientAgentId),
284
+ scope: scope,
285
+ expiresAt: coerceOptionalString(args.expiresAt),
286
+ });
287
+ return { result: grant, isError: false };
288
+ }
289
+ case "revoke_share": {
290
+ if (!grants)
291
+ return { result: NO_GRANT_STORE, isError: true };
292
+ const result = await (0, grants_1.revokeShare)(grants, coerceString(args.grantId));
293
+ return { result, isError: result.ok === false };
294
+ }
295
+ case "list_shares": {
296
+ if (!grants)
297
+ return { result: NO_GRANT_STORE, isError: true };
298
+ // Fork D compat seam (b): accept `subjectKey`, falling back to the legacy
299
+ // `subjectFriendId` filter arg.
300
+ const result = await (0, grants_1.listShares)(grants, {
301
+ subjectKey: coerceOptionalString(args.subjectKey) ?? coerceOptionalString(args.subjectFriendId),
302
+ recipientAgentId: coerceOptionalString(args.recipientAgentId),
303
+ effectiveOnly: coerceBool(args.effectiveOnly),
304
+ });
305
+ return { result, isError: false };
306
+ }
307
+ case "record_mission": {
308
+ if (!missions)
309
+ return { result: NO_MISSION_STORE, isError: true };
310
+ const input = {
311
+ missionKey: coerceString(args.missionKey),
312
+ title: coerceOptionalString(args.title),
313
+ status: coerceOptionalString(args.status),
314
+ participants: parseMaybeJson(args.participants),
315
+ learnings: parseMaybeJson(args.learnings),
316
+ outcomes: parseMaybeJson(args.outcomes),
317
+ };
318
+ const record = await (0, missions_1.recordMission)(missions, input);
319
+ return { result: record, isError: false };
320
+ }
321
+ case "get_mission": {
322
+ if (!missions)
323
+ return { result: NO_MISSION_STORE, isError: true };
324
+ const record = await missions.get(coerceString(args.missionId));
325
+ if (!record) {
326
+ return { result: { ok: false, status: "not_found", message: "mission record not found" }, isError: true };
327
+ }
328
+ return { result: record, isError: false };
329
+ }
330
+ case "list_missions": {
331
+ if (!missions)
332
+ return { result: NO_MISSION_STORE, isError: true };
333
+ const all = await missions.listAll();
334
+ const limit = coerceInt(args.limit);
335
+ const result = limit !== undefined ? all.slice(0, limit) : all;
336
+ return { result, isError: false };
337
+ }
338
+ case "share_mission": {
339
+ // Producer. Self identity comes from whoami (the dispatch is store-only);
340
+ // the mission is named by its missionKey inside the library. Gated on BOTH a
341
+ // GrantStore (consent) and a MissionStore.
342
+ if (!missions || !grants)
343
+ return { result: NO_MISSION_STORE, isError: true };
344
+ const scope = coerceString(args.scope);
345
+ if (scope !== "mission" && scope !== "outcomes") {
346
+ return {
347
+ result: { ok: false, status: "invalid", message: `unrecognized mission scope '${scope}' — use mission or outcomes` },
348
+ isError: true,
349
+ };
350
+ }
351
+ const self = await (0, whoami_1.whoami)(store);
352
+ const selfAgentId = self.selfFriendId ?? "";
353
+ const result = await (0, mission_share_1.prepareMissionShare)(missions, store, grants, {
354
+ missionId: coerceString(args.missionId),
355
+ toAgentId: coerceString(args.toAgentId),
356
+ scope,
357
+ selfAgentId,
358
+ proof: coerceOptionalString(args.proof),
359
+ });
360
+ return { result, isError: result.ok === false };
361
+ }
362
+ case "import_mission": {
363
+ if (!missions)
364
+ return { result: NO_MISSION_STORE, isError: true };
365
+ const envelope = parseMaybeJson(args.envelope);
366
+ if (!envelope || typeof envelope !== "object") {
367
+ return { result: { ok: false, status: "invalid", message: "an envelope object is required" }, isError: true };
368
+ }
369
+ const result = await (0, mission_share_1.importMissionShare)(missions, {
370
+ envelope,
371
+ fromAgentId: coerceString(args.fromAgentId),
372
+ trustOfSource: coerceString(args.trustOfSource),
373
+ });
374
+ return { result, isError: result.ok === false };
375
+ }
376
+ case "coordinate": {
377
+ // Producer (brick 5). Self identity comes from whoami (the dispatch is
378
+ // store-only); the mission is named by its missionKey inside the library.
379
+ // Gated on BOTH a GrantStore (consent via the "coordinate" scope) and a
380
+ // MissionStore — like share_mission.
381
+ if (!missions || !grants)
382
+ return { result: NO_MISSION_STORE, isError: true };
383
+ const intent = coerceString(args.intent);
384
+ if (!(0, types_2.isCoordinationIntent)(intent)) {
385
+ return {
386
+ result: { ok: false, status: "invalid", message: `unrecognized coordination intent '${intent}' — use request, offer, accept, decline, or handoff` },
387
+ isError: true,
388
+ };
389
+ }
390
+ const self = await (0, whoami_1.whoami)(store);
391
+ const selfAgentId = self.selfFriendId ?? "";
392
+ const result = await (0, coordination_1.prepareCoordination)(missions, store, grants, {
393
+ missionId: coerceString(args.missionId),
394
+ toAgentId: coerceString(args.toAgentId),
395
+ intent,
396
+ note: coerceOptionalString(args.note),
397
+ proposedAssignee: parseMaybeJson(args.proposedAssignee),
398
+ selfAgentId,
399
+ proof: coerceOptionalString(args.proof),
400
+ });
401
+ return { result, isError: result.ok === false };
402
+ }
403
+ case "import_coordination": {
404
+ if (!missions)
405
+ return { result: NO_MISSION_STORE, isError: true };
406
+ const envelope = parseMaybeJson(args.envelope);
407
+ if (!envelope || typeof envelope !== "object") {
408
+ return { result: { ok: false, status: "invalid", message: "an envelope object is required" }, isError: true };
409
+ }
410
+ const result = await (0, coordination_1.importCoordination)(missions, {
411
+ envelope,
412
+ fromAgentId: coerceString(args.fromAgentId),
413
+ trustOfSource: coerceString(args.trustOfSource),
414
+ });
415
+ return { result, isError: result.ok === false };
416
+ }
417
+ case "get_coordination": {
418
+ // Read lens (brick 5), like get_mission: return the mission's coordination
419
+ // sub-object (assignee + log), or the empty default when unset.
420
+ if (!missions)
421
+ return { result: NO_MISSION_STORE, isError: true };
422
+ const record = await missions.get(coerceString(args.missionId));
423
+ if (!record) {
424
+ return { result: { ok: false, status: "not_found", message: "mission record not found" }, isError: true };
425
+ }
426
+ return { result: record.coordination ?? { assignee: undefined, log: [] }, isError: false };
427
+ }
428
+ default: {
429
+ return { result: { error: `Unknown tool: ${name}` }, isError: true };
430
+ }
431
+ }
432
+ }
@@ -0,0 +1,6 @@
1
+ export { createFriendsMcpServer } from "./server";
2
+ export type { FriendsMcpServer, FriendsMcpServerOptions } from "./server";
3
+ export { getToolSchemas } from "./schemas";
4
+ export type { McpToolSchema } from "./schemas";
5
+ export { runMain } from "./run-main";
6
+ export type { RunMainIo } from "./run-main";
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMain = exports.getToolSchemas = exports.createFriendsMcpServer = void 0;
4
+ // @ouro.bot/friends/mcp — the MCP server surface.
5
+ //
6
+ // A thin, harness-agnostic MCP tool surface over the friends library: dual
7
+ // stdio framing, a flat tool → library-fn dispatch, and NO agent turn. Consumed
8
+ // via the `friends-mcp` bin or embedded directly through `createFriendsMcpServer`.
9
+ var server_1 = require("./server");
10
+ Object.defineProperty(exports, "createFriendsMcpServer", { enumerable: true, get: function () { return server_1.createFriendsMcpServer; } });
11
+ var schemas_1 = require("./schemas");
12
+ Object.defineProperty(exports, "getToolSchemas", { enumerable: true, get: function () { return schemas_1.getToolSchemas; } });
13
+ var run_main_1 = require("./run-main");
14
+ Object.defineProperty(exports, "runMain", { enumerable: true, get: function () { return run_main_1.runMain; } });
@@ -0,0 +1,7 @@
1
+ import type { FriendsMcpServer } from "./server";
2
+ export interface RunMainIo {
3
+ stdin: NodeJS.ReadableStream;
4
+ stdout: NodeJS.WritableStream;
5
+ onError: (message: string) => void;
6
+ }
7
+ export declare function runMain(argv: string[], env: NodeJS.ProcessEnv, io: RunMainIo): FriendsMcpServer | null;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMain = runMain;
4
+ // runMain — the covered core of the `friends-mcp` bin.
5
+ //
6
+ // Resolves the friends directory from `--dir <path>` (flag wins) or the
7
+ // `FRIENDS_DIR` environment variable, constructs a FileFriendStore over it, and
8
+ // starts an MCP server on the given streams. If neither source supplies a
9
+ // directory, it reports via `onError` and constructs nothing (returns null).
10
+ const observability_1 = require("../observability");
11
+ const file_bundle_1 = require("../file-bundle");
12
+ const server_1 = require("./server");
13
+ /** Parse `--dir <value>` from argv. Returns the value, or undefined if the flag
14
+ * is absent or has no following value. */
15
+ function parseDirFlag(argv) {
16
+ const idx = argv.indexOf("--dir");
17
+ if (idx === -1)
18
+ return undefined;
19
+ const value = argv[idx + 1];
20
+ if (value === undefined)
21
+ return undefined;
22
+ return value;
23
+ }
24
+ function runMain(argv, env, io) {
25
+ const flagDir = parseDirFlag(argv);
26
+ const dir = flagDir ?? env.FRIENDS_DIR;
27
+ const source = flagDir !== undefined ? "flag" : "env";
28
+ if (!dir) {
29
+ io.onError("friends-mcp requires --dir <path> or FRIENDS_DIR");
30
+ return null;
31
+ }
32
+ (0, observability_1.emitNervesEvent)({
33
+ component: "clients",
34
+ event: "clients.mcp_run_main",
35
+ message: "friends mcp run-main",
36
+ meta: { source },
37
+ });
38
+ // The consent-grant + mission collections are sibling `_grants/` + `_missions/`
39
+ // dirs under the friends dir, so the single `--dir` wires the whole substrate
40
+ // (friends + consent + missions). `openFileBundle` encapsulates that convention.
41
+ const { store, grants, missions } = (0, file_bundle_1.openFileBundle)(dir);
42
+ const server = (0, server_1.createFriendsMcpServer)({ store, grants, missions, stdin: io.stdin, stdout: io.stdout });
43
+ server.start();
44
+ return server;
45
+ }
@@ -0,0 +1,10 @@
1
+ export interface McpToolSchema {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: Record<string, unknown>;
7
+ required?: string[];
8
+ };
9
+ }
10
+ export declare function getToolSchemas(): McpToolSchema[];