publish_intent_card
Publish your intent card to the Mingle network—specify your needs and offers—and receive top matches instantly.
Instructions
Publish your profile to the Mingle network — what you're looking for and what you can offer. Cards are Ed25519 signed with your persistent identity and expire after 48h. Returns your top matches immediately.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Your name or alias | |
| topic | No | What you're working on (short summary) | |
| needs | No | What you're looking for (plain text list) | |
| offers | No | What you can provide (plain text list) | |
| context | No | Rich context for better matching (private — never shown to others) | |
| open_to | No | Open to (e.g. 'introductions', 'partnerships') | |
| hours | No | Hours until card expires (default 48) |
Implementation Reference
- src/index.ts:110-180 (handler)The async handler function that executes the publish_intent_card tool logic: validates inputs, builds card payload, signs with Ed25519, POSTs to /api/cards, caches locally, fetches digest, and returns top matches.
async (args) => { const MAX_FIELD_LEN = 200; const MAX_ITEMS = 5; if (args.name.length > 100) return { content: [{ type: "text" as const, text: "Name too long (max 100 chars)" }], isError: true }; if ((args.needs?.length || 0) > MAX_ITEMS) return { content: [{ type: "text" as const, text: `Too many needs (max ${MAX_ITEMS})` }], isError: true }; if ((args.offers?.length || 0) > MAX_ITEMS) return { content: [{ type: "text" as const, text: `Too many offers (max ${MAX_ITEMS})` }], isError: true }; for (const item of [...(args.needs || []), ...(args.offers || [])]) { if (item.length > MAX_FIELD_LEN) return { content: [{ type: "text" as const, text: `Item too long (max ${MAX_FIELD_LEN} chars)` }], isError: true }; } if (args.context && args.context.length > 1000) return { content: [{ type: "text" as const, text: "Context too long (max 1000 chars)" }], isError: true }; // Build card manually (not via createIntentCard) so signature covers all fields const card: Record<string, any> = { cardId: `card-${agentId}-${Date.now()}`, agentId, publicKey: keys.publicKey, principalAlias: args.name, topic: args.topic || "", needs: (args.needs || []).map(desc => ({ description: desc, category: "general" })), offers: (args.offers || []).map(desc => ({ description: desc, category: "general" })), openTo: args.open_to || ["introductions", "collaboration"], context: args.context || "", provenance: "explicit", confidence: 1.0, source: "organic", expiresAt: new Date(Date.now() + (args.hours || 48) * 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), }; // Sign the full card (API strips signature, canonicalizes rest, verifies) card.signature = sign(canonicalize(card), keys.privateKey); try { const result = await api("/api/cards", { method: "POST", body: JSON.stringify(card) }); if (result.error) return { content: [{ type: "text" as const, text: `Failed: ${result.error}` }], isError: true }; // Cache card locally for offline resilience cacheCard({ cardId: result.cardId, topic: args.topic, needs: args.needs, offers: args.offers, expiresAt: result.expiresAt }); const digest = await fetchDigest(); return { content: [{ type: "text" as const, text: withDigest({ published: true, cardId: result.cardId, name: args.name, topic: args.topic, needs: (args.needs || []).length, offers: (args.offers || []).length, expiresAt: result.expiresAt, networkSize: result.networkSize, topMatches: classifyMatches(result.topMatches || [], prefs.mode).slice(0, 3).map((m: any) => ({ name: sanitize(m.name || m.agentId), score: m.score, mutual: m.mutual, confidence: m.confidence, surfacing: m.surfacing, needMatch: sanitize(m.needMatch), offerMatch: sanitize(m.offerMatch), })), matchingVersion: result.matchingVersion || "semantic-v1", }, digest), }], }; } catch (e: any) { return { content: [{ type: "text" as const, text: `Network error: ${e.message}` }], isError: true }; } } ); - src/index.ts:101-109 (schema)Zod schema definitions for publish_intent_card inputs: name (string), topic (optional), needs (optional string array), offers (optional string array), context (optional string), open_to (optional string array), hours (number, default 48).
{ name: z.string().describe("Your name or alias"), topic: z.string().optional().describe("What you're working on (short summary)"), needs: z.array(z.string()).optional().describe("What you're looking for (plain text list)"), offers: z.array(z.string()).optional().describe("What you can provide (plain text list)"), context: z.string().optional().describe("Rich context for better matching (private — never shown to others)"), open_to: z.array(z.string()).optional().describe("Open to (e.g. 'introductions', 'partnerships')"), hours: z.number().default(48).describe("Hours until card expires (default 48)"), }, - src/index.ts:98-180 (registration)Registration of the 'publish_intent_card' tool on the MCP server via server.tool() with name, description, schema, and handler.
server.tool( "publish_intent_card", "Publish your profile to the Mingle network — what you're looking for and what you can offer. Cards are Ed25519 signed with your persistent identity and expire after 48h. Returns your top matches immediately.", { name: z.string().describe("Your name or alias"), topic: z.string().optional().describe("What you're working on (short summary)"), needs: z.array(z.string()).optional().describe("What you're looking for (plain text list)"), offers: z.array(z.string()).optional().describe("What you can provide (plain text list)"), context: z.string().optional().describe("Rich context for better matching (private — never shown to others)"), open_to: z.array(z.string()).optional().describe("Open to (e.g. 'introductions', 'partnerships')"), hours: z.number().default(48).describe("Hours until card expires (default 48)"), }, async (args) => { const MAX_FIELD_LEN = 200; const MAX_ITEMS = 5; if (args.name.length > 100) return { content: [{ type: "text" as const, text: "Name too long (max 100 chars)" }], isError: true }; if ((args.needs?.length || 0) > MAX_ITEMS) return { content: [{ type: "text" as const, text: `Too many needs (max ${MAX_ITEMS})` }], isError: true }; if ((args.offers?.length || 0) > MAX_ITEMS) return { content: [{ type: "text" as const, text: `Too many offers (max ${MAX_ITEMS})` }], isError: true }; for (const item of [...(args.needs || []), ...(args.offers || [])]) { if (item.length > MAX_FIELD_LEN) return { content: [{ type: "text" as const, text: `Item too long (max ${MAX_FIELD_LEN} chars)` }], isError: true }; } if (args.context && args.context.length > 1000) return { content: [{ type: "text" as const, text: "Context too long (max 1000 chars)" }], isError: true }; // Build card manually (not via createIntentCard) so signature covers all fields const card: Record<string, any> = { cardId: `card-${agentId}-${Date.now()}`, agentId, publicKey: keys.publicKey, principalAlias: args.name, topic: args.topic || "", needs: (args.needs || []).map(desc => ({ description: desc, category: "general" })), offers: (args.offers || []).map(desc => ({ description: desc, category: "general" })), openTo: args.open_to || ["introductions", "collaboration"], context: args.context || "", provenance: "explicit", confidence: 1.0, source: "organic", expiresAt: new Date(Date.now() + (args.hours || 48) * 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), }; // Sign the full card (API strips signature, canonicalizes rest, verifies) card.signature = sign(canonicalize(card), keys.privateKey); try { const result = await api("/api/cards", { method: "POST", body: JSON.stringify(card) }); if (result.error) return { content: [{ type: "text" as const, text: `Failed: ${result.error}` }], isError: true }; // Cache card locally for offline resilience cacheCard({ cardId: result.cardId, topic: args.topic, needs: args.needs, offers: args.offers, expiresAt: result.expiresAt }); const digest = await fetchDigest(); return { content: [{ type: "text" as const, text: withDigest({ published: true, cardId: result.cardId, name: args.name, topic: args.topic, needs: (args.needs || []).length, offers: (args.offers || []).length, expiresAt: result.expiresAt, networkSize: result.networkSize, topMatches: classifyMatches(result.topMatches || [], prefs.mode).slice(0, 3).map((m: any) => ({ name: sanitize(m.name || m.agentId), score: m.score, mutual: m.mutual, confidence: m.confidence, surfacing: m.surfacing, needMatch: sanitize(m.needMatch), offerMatch: sanitize(m.offerMatch), })), matchingVersion: result.matchingVersion || "semantic-v1", }, digest), }], }; } catch (e: any) { return { content: [{ type: "text" as const, text: `Network error: ${e.message}` }], isError: true }; } } ); - src/identity.ts:60-63 (helper)cacheCard helper that persists the published card to ~/.mingle/last-card.json for offline resilience.
export function cacheCard(card: any): void { ensureDir(); writeFileSync(LAST_CARD_PATH, JSON.stringify(card, null, 2)); }