b2match-mcp 0.2.0 → 0.2.1
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/README.md +1 -1
- package/dist/server.js +83 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ This package is designed to be run locally by the user via `npx` and stdio MCP t
|
|
|
25
25
|
|
|
26
26
|
## Install / run
|
|
27
27
|
|
|
28
|
-
Requires Node.js
|
|
28
|
+
Requires Node.js 22 or newer. On macOS with Apple Silicon, the recommended install path is `npx`; no native build step is required for the auth storage path.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx -y b2match-mcp
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SIDE_EFFECT_READS_ENV, conversationMessagesGate, notificationListGate }
|
|
|
8
8
|
import { assertNoUnacknowledgedSideEffectReads, assertReadOnlyGraphQL, UnsafeGraphQLOperationError } from "./queryGuard.js";
|
|
9
9
|
import { packageName, packageVersion } from "./packageVersion.js";
|
|
10
10
|
import { EventInput, PagingInput, authErrorResult, errorResult, notFoundResult, textResult } from "./tooling.js";
|
|
11
|
+
const PATH_DESC = "Optional event menu path (e.g. /components/<id>) from get_event navigation; omit to resolve it automatically.";
|
|
11
12
|
const readOnlyAnnotations = {
|
|
12
13
|
readOnlyHint: true,
|
|
13
14
|
destructiveHint: false,
|
|
@@ -34,7 +35,7 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
34
35
|
};
|
|
35
36
|
server.registerTool("auth_status", {
|
|
36
37
|
title: "B2match auth status",
|
|
37
|
-
description: "Show whether a local B2match token is available and, when valid, who it belongs to. Never returns the token.",
|
|
38
|
+
description: "Show whether a local B2match token is available and, when valid, who it belongs to. Never returns the token. Call this first when an authenticated tool (my_*, list_my_events, conversations, notifications) reports a login error; public tools work without a token.",
|
|
38
39
|
annotations: readOnlyAnnotations
|
|
39
40
|
}, authTool(async () => {
|
|
40
41
|
const local = await authStore.status();
|
|
@@ -43,18 +44,18 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
43
44
|
}));
|
|
44
45
|
server.registerTool("list_my_events", {
|
|
45
46
|
title: "List my B2match events",
|
|
46
|
-
description: "List events
|
|
47
|
+
description: "List events the logged-in viewer has joined, with ids/slugs usable as the event argument elsewhere. Requires login; use get_event for any single public event instead.",
|
|
47
48
|
annotations: readOnlyAnnotations
|
|
48
49
|
}, authTool(async () => api.listMyEvents()));
|
|
49
50
|
server.registerTool("get_event", {
|
|
50
51
|
title: "Get B2match event",
|
|
51
|
-
description: "Fetch public event metadata, dates, registration window, login methods, and navigation.",
|
|
52
|
+
description: "Fetch public event metadata, dates, registration window, login methods, and navigation menu paths. Start here to resolve an event before calling the other event tools; use list_my_events to find events you joined. Works without login.",
|
|
52
53
|
inputSchema: EventInput.shape,
|
|
53
54
|
annotations: readOnlyAnnotations
|
|
54
55
|
}, async ({ event }) => textResult((await api.getEvent(event)).raw));
|
|
55
56
|
server.registerTool("my_participation", {
|
|
56
57
|
title: "Get my event participation",
|
|
57
|
-
description: "Fetch
|
|
58
|
+
description: "Fetch the logged-in viewer's own participant status, activation, permissions, organisation pages, and opportunities for one event. Use get_participant for someone else's public profile.",
|
|
58
59
|
inputSchema: EventInput.shape,
|
|
59
60
|
annotations: readOnlyAnnotations
|
|
60
61
|
}, authTool(async ({ event }) => api.myParticipation(event)));
|
|
@@ -62,44 +63,51 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
62
63
|
title: "List B2match participants",
|
|
63
64
|
description: "List public participants as a compact summary. Set detail=true for the fuller record; use get_participant for one profile.",
|
|
64
65
|
inputSchema: EventInput.merge(PagingInput).extend({
|
|
65
|
-
query: z.string().optional(),
|
|
66
|
-
detail: z
|
|
66
|
+
query: z.string().optional().describe("Free-text search on participant name, organisation, or profile text."),
|
|
67
|
+
detail: z
|
|
68
|
+
.boolean()
|
|
69
|
+
.default(false)
|
|
70
|
+
.describe("true returns the full participant records instead of compact summaries (larger output).")
|
|
67
71
|
}).shape,
|
|
68
72
|
annotations: readOnlyAnnotations
|
|
69
73
|
}, async ({ event, first, after, query, detail }) => textResult(await api.listParticipants(event, { first, after, query, detail })));
|
|
70
74
|
server.registerTool("get_participant", {
|
|
71
75
|
title: "Get B2match participant",
|
|
72
|
-
description: "Fetch
|
|
73
|
-
inputSchema: EventInput.extend({
|
|
76
|
+
description: "Fetch one participant's full public profile without sending B2match's profile-view tracking mutation. Use list_participants to search or find ids; use my_participation for your own status.",
|
|
77
|
+
inputSchema: EventInput.extend({
|
|
78
|
+
participant_id: z
|
|
79
|
+
.string()
|
|
80
|
+
.describe("Participant id from list_participants, list_session_attendees, or meeting_recommendations.")
|
|
81
|
+
}).shape,
|
|
74
82
|
annotations: readOnlyAnnotations
|
|
75
83
|
}, async ({ event, participant_id }) => textResult(await api.getParticipant(event, participant_id)));
|
|
76
84
|
server.registerTool("list_organisations", {
|
|
77
85
|
title: "List B2match organisations",
|
|
78
|
-
description: "List
|
|
79
|
-
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional() }).shape,
|
|
86
|
+
description: "List organisation pages (participations) of an event, paginated. Use get_organisation for one page, list_participants for people. Resolves the menu path automatically; pass path to override.",
|
|
87
|
+
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional().describe(PATH_DESC) }).shape,
|
|
80
88
|
annotations: readOnlyAnnotations
|
|
81
89
|
}, async ({ event, path, first, after }) => textResult(await api.listOrganisations(event, { path, first, after })));
|
|
82
90
|
server.registerTool("get_organisation", {
|
|
83
91
|
title: "Get B2match organisation",
|
|
84
|
-
description: "Fetch
|
|
92
|
+
description: "Fetch one organisation page (a participation: organisation details plus its representatives). Use list_organisations to find participation ids; use get_participant for a single person. Resolves the menu path automatically; pass path to override.",
|
|
85
93
|
inputSchema: EventInput.extend({
|
|
86
|
-
participation_id: z.string(),
|
|
87
|
-
path: z.string().optional()
|
|
94
|
+
participation_id: z.string().describe("Participation id from list_organisations or my_participation."),
|
|
95
|
+
path: z.string().optional().describe(PATH_DESC)
|
|
88
96
|
}).shape,
|
|
89
97
|
annotations: readOnlyAnnotations
|
|
90
98
|
}, async ({ event, participation_id, path }) => textResult(await api.getOrganisation(event, participation_id, path)));
|
|
91
99
|
server.registerTool("list_opportunities", {
|
|
92
100
|
title: "List B2match opportunities",
|
|
93
|
-
description: "List marketplace opportunities. Resolves the menu path automatically; pass path to override.",
|
|
94
|
-
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional() }).shape,
|
|
101
|
+
description: "List published marketplace opportunities (offers/requests) of an event, paginated. Use get_opportunity for one entry's full details. Resolves the menu path automatically; pass path to override.",
|
|
102
|
+
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional().describe(PATH_DESC) }).shape,
|
|
95
103
|
annotations: readOnlyAnnotations
|
|
96
104
|
}, async ({ event, path, first, after }) => textResult(await api.listOpportunities(event, { path, first, after })));
|
|
97
105
|
server.registerTool("get_opportunity", {
|
|
98
106
|
title: "Get B2match opportunity",
|
|
99
107
|
description: "Fetch a marketplace opportunity. Use the id or url from list_opportunities (published marketplace ids differ from opportunity ids on my_participation). Resolves the marketplace menu path automatically; pass path to override.",
|
|
100
108
|
inputSchema: EventInput.extend({
|
|
101
|
-
opportunity_id: z.string(),
|
|
102
|
-
path: z.string().optional()
|
|
109
|
+
opportunity_id: z.string().describe("Published opportunity id or URL from list_opportunities."),
|
|
110
|
+
path: z.string().optional().describe(PATH_DESC)
|
|
103
111
|
}).shape,
|
|
104
112
|
annotations: readOnlyAnnotations
|
|
105
113
|
}, async ({ event, opportunity_id, path }) => {
|
|
@@ -115,69 +123,90 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
115
123
|
});
|
|
116
124
|
server.registerTool("get_agenda", {
|
|
117
125
|
title: "Get B2match agenda",
|
|
118
|
-
description: "Fetch agenda sessions and networking blocks. Resolves the menu path automatically; pass path to override.",
|
|
119
|
-
inputSchema: EventInput.extend({ path: z.string().optional() }).shape,
|
|
126
|
+
description: "Fetch the event agenda: sessions and networking blocks with times. Use get_session for one session's details and my_schedule for your personal schedule. Resolves the menu path automatically; pass path to override.",
|
|
127
|
+
inputSchema: EventInput.extend({ path: z.string().optional().describe(PATH_DESC) }).shape,
|
|
120
128
|
annotations: readOnlyAnnotations
|
|
121
129
|
}, async ({ event, path }) => textResult(await api.getAgenda(event, path)));
|
|
122
130
|
server.registerTool("get_session", {
|
|
123
131
|
title: "Get B2match session",
|
|
124
|
-
description: "Fetch
|
|
132
|
+
description: "Fetch one agenda session with its details and a first page of attendees. Provide session_id, or path to a session URL; path alone is enough. Use list_session_attendees to page through attendees only, get_agenda to find sessions.",
|
|
125
133
|
inputSchema: EventInput.extend({
|
|
126
|
-
session_id: z.string().optional(),
|
|
127
|
-
path: z.string().optional(),
|
|
128
|
-
attendees_first: z.number().int().min(1).max(50).default(20),
|
|
129
|
-
attendees_after: z.string().optional()
|
|
134
|
+
session_id: z.string().optional().describe("Session id from get_agenda. Optional when path is set."),
|
|
135
|
+
path: z.string().optional().describe("Session URL path from get_agenda; alternative to session_id."),
|
|
136
|
+
attendees_first: z.number().int().min(1).max(50).default(20).describe("Attendee page size, capped at 50."),
|
|
137
|
+
attendees_after: z.string().optional().describe("Attendee pagination cursor from a previous response.")
|
|
130
138
|
}).shape,
|
|
131
139
|
annotations: readOnlyAnnotations
|
|
132
140
|
}, async ({ event, session_id, path, attendees_first, attendees_after }) => textResult(await api.getSession(event, session_id, { path, first: attendees_first, after: attendees_after })));
|
|
133
141
|
server.registerTool("list_session_attendees", {
|
|
134
142
|
title: "List B2match session attendees",
|
|
135
|
-
description: "
|
|
143
|
+
description: "Page through the attendees of one agenda session. Returns only the attendee list with a cursor, not the session details; use get_session for those. Provide session_id or path (from get_agenda).",
|
|
136
144
|
inputSchema: EventInput.extend({
|
|
137
|
-
session_id: z.string().optional(),
|
|
138
|
-
path: z.string().optional(),
|
|
139
|
-
first: z.number().int().min(1).max(50).default(20),
|
|
140
|
-
after: z.string().optional()
|
|
145
|
+
session_id: z.string().optional().describe("Session id from get_agenda. Optional when path is set."),
|
|
146
|
+
path: z.string().optional().describe("Session URL path from get_agenda; alternative to session_id."),
|
|
147
|
+
first: z.number().int().min(1).max(50).default(20).describe("Page size, capped at 50."),
|
|
148
|
+
after: z.string().optional().describe("Opaque pagination cursor from a previous response.")
|
|
141
149
|
}).shape,
|
|
142
150
|
annotations: readOnlyAnnotations
|
|
143
151
|
}, async ({ event, session_id, path, first, after }) => textResult(await api.listSessionAttendees(event, session_id, { path, first, after })));
|
|
144
152
|
server.registerTool("list_speakers", {
|
|
145
153
|
title: "List B2match speakers",
|
|
146
|
-
description: "List event speakers. Resolves the menu path automatically; pass path to override.",
|
|
147
|
-
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional() }).shape,
|
|
154
|
+
description: "List the speakers shown on the event's speakers page, paginated. Use list_participants for all registered participants. Resolves the menu path automatically; pass path to override.",
|
|
155
|
+
inputSchema: EventInput.merge(PagingInput).extend({ path: z.string().optional().describe(PATH_DESC) }).shape,
|
|
148
156
|
annotations: readOnlyAnnotations
|
|
149
157
|
}, async ({ event, path, first, after }) => textResult(await api.listSpeakers(event, { path, first, after })));
|
|
150
158
|
server.registerTool("my_schedule", {
|
|
151
159
|
title: "Get my B2match schedule",
|
|
152
|
-
description: "Fetch the
|
|
153
|
-
inputSchema: EventInput.extend({
|
|
160
|
+
description: "Fetch the logged-in viewer's personal event schedule (booked sessions and meetings). Use get_agenda for the full public agenda and my_meetings for meeting requests by status. The PDF agenda URL is omitted unless include_pdf_link is true.",
|
|
161
|
+
inputSchema: EventInput.extend({
|
|
162
|
+
include_pdf_link: z.boolean().default(false).describe("true adds the downloadable PDF agenda URL.")
|
|
163
|
+
}).shape,
|
|
154
164
|
annotations: readOnlyAnnotations
|
|
155
165
|
}, authTool(async ({ event, include_pdf_link }) => api.mySchedule(event, include_pdf_link)));
|
|
156
166
|
server.registerTool("my_meetings", {
|
|
157
167
|
title: "Get my B2match meetings",
|
|
158
|
-
description: "Fetch
|
|
168
|
+
description: "Fetch the logged-in viewer's meetings and meeting requests, optionally one status bucket. Unfiltered responses include a compact availability summary (merged free ranges per day) unless detail=true. Use my_schedule for the overall timetable and find_meeting_slots to find free times with others.",
|
|
159
169
|
inputSchema: EventInput.extend({
|
|
160
|
-
status: z
|
|
161
|
-
|
|
170
|
+
status: z
|
|
171
|
+
.enum(["accepted", "receivedInvitations", "sentInvitations", "canceled"])
|
|
172
|
+
.optional()
|
|
173
|
+
.describe("Return only this bucket; omit for all buckets."),
|
|
174
|
+
detail: z
|
|
175
|
+
.boolean()
|
|
176
|
+
.default(false)
|
|
177
|
+
.describe("true returns the full availability timeslot grid instead of the summary.")
|
|
162
178
|
}).shape,
|
|
163
179
|
annotations: readOnlyAnnotations
|
|
164
180
|
}, authTool(async ({ event, status, detail }) => api.myMeetings(event, status, detail)));
|
|
165
181
|
server.registerTool("my_availability", {
|
|
166
182
|
title: "Get my B2match availability",
|
|
167
|
-
description: "Fetch
|
|
183
|
+
description: "Fetch the logged-in viewer's own meeting availability. Default is merged free ranges per day; set detail=true for the full timeslot grid. Use find_meeting_slots for slots shared with specific participants.",
|
|
168
184
|
inputSchema: EventInput.extend({
|
|
169
|
-
detail: z.boolean().default(false)
|
|
185
|
+
detail: z.boolean().default(false).describe("true returns the full timeslot grid instead of merged ranges.")
|
|
170
186
|
}).shape,
|
|
171
187
|
annotations: readOnlyAnnotations
|
|
172
188
|
}, authTool(async ({ event, detail }) => api.myAvailability(event, detail)));
|
|
173
189
|
server.registerTool("find_meeting_slots", {
|
|
174
190
|
title: "Find B2match meeting slots",
|
|
175
|
-
description: "
|
|
191
|
+
description: "Find free meeting slots shared by the logged-in viewer and selected participants on one day. Read-only: does not book or request a meeting. Use my_availability for your own free time only.",
|
|
176
192
|
inputSchema: EventInput.extend({
|
|
177
|
-
participant_ids: z
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
participant_ids: z
|
|
194
|
+
.array(z.string())
|
|
195
|
+
.min(1)
|
|
196
|
+
.max(10)
|
|
197
|
+
.describe("1-10 participant ids (from list_participants) to meet with."),
|
|
198
|
+
day: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Day to search, as a days[].date value from my_availability; omit for the default day."),
|
|
202
|
+
duration: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("B2match MeetingDuration enum value (e.g. THIRTHY, B2match's spelling); omit for the event default."),
|
|
206
|
+
is_long_term_networking: z
|
|
207
|
+
.boolean()
|
|
208
|
+
.default(false)
|
|
209
|
+
.describe("true searches the event's long-term networking calendar instead of the main meeting days.")
|
|
181
210
|
}).shape,
|
|
182
211
|
annotations: readOnlyAnnotations
|
|
183
212
|
}, authTool(async ({ event, participant_ids, day, duration, is_long_term_networking }) => api.findMeetingSlots(event, participant_ids, {
|
|
@@ -187,13 +216,13 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
187
216
|
})));
|
|
188
217
|
server.registerTool("meeting_recommendations", {
|
|
189
218
|
title: "Get B2match meeting recommendations",
|
|
190
|
-
description: "Fetch
|
|
219
|
+
description: "Fetch B2match's recommended and newest participants for the logged-in viewer to meet. Use list_participants to search all participants and find_meeting_slots to check shared free time.",
|
|
191
220
|
inputSchema: EventInput.shape,
|
|
192
221
|
annotations: readOnlyAnnotations
|
|
193
222
|
}, authTool(async ({ event }) => api.meetingRecommendations(event)));
|
|
194
223
|
server.registerTool("list_conversations", {
|
|
195
224
|
title: "List B2match conversations",
|
|
196
|
-
description: "List
|
|
225
|
+
description: "List the logged-in viewer's conversations with last-message previews, paginated. Does not mark conversations as read; prefer it over get_conversation unless full message history is needed.",
|
|
197
226
|
inputSchema: EventInput.merge(PagingInput).shape,
|
|
198
227
|
annotations: readOnlyAnnotations
|
|
199
228
|
}, authTool(async ({ event, first, after }) => api.listConversations(event, { first, after })));
|
|
@@ -201,13 +230,13 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
201
230
|
title: "Get B2match conversation",
|
|
202
231
|
description: `Fetch conversation messages. B2match marks that conversation as read when messagesV2 is fetched, even though this client never sends MarkConversationAsSeen. Disabled unless ${SIDE_EFFECT_READS_ENV}=1 and acknowledge_marks_read is true. Use list_conversations for last-message previews. Unknown ids return a not-found error.`,
|
|
203
232
|
inputSchema: z.object({
|
|
204
|
-
conversation_id: z.string().uuid(),
|
|
233
|
+
conversation_id: z.string().uuid().describe("Conversation UUID from list_conversations."),
|
|
205
234
|
acknowledge_marks_read: z
|
|
206
235
|
.boolean()
|
|
207
236
|
.default(false)
|
|
208
237
|
.describe("Required with B2MATCH_ALLOW_SIDE_EFFECT_READS=1; fetching messages marks the conversation as read.")
|
|
209
238
|
}).shape,
|
|
210
|
-
annotations: { ...readOnlyAnnotations, idempotentHint: false }
|
|
239
|
+
annotations: { ...readOnlyAnnotations, readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
211
240
|
}, async ({ conversation_id, acknowledge_marks_read }) => {
|
|
212
241
|
const blocked = conversationMessagesGate(acknowledge_marks_read);
|
|
213
242
|
if (blocked) {
|
|
@@ -235,7 +264,7 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
235
264
|
.default(false)
|
|
236
265
|
.describe("Required with B2MATCH_ALLOW_SIDE_EFFECT_READS=1 to fetch the full list; that request resets unseen.")
|
|
237
266
|
}).shape,
|
|
238
|
-
annotations: { ...readOnlyAnnotations, idempotentHint: false }
|
|
267
|
+
annotations: { ...readOnlyAnnotations, readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
239
268
|
}, async ({ event, first, after, acknowledge_marks_seen }) => {
|
|
240
269
|
try {
|
|
241
270
|
if (acknowledge_marks_seen) {
|
|
@@ -259,9 +288,12 @@ export function createMcpServer(authStore = new AuthStore()) {
|
|
|
259
288
|
title: "Raw read-only B2match GraphQL",
|
|
260
289
|
description: `Run a raw B2match GraphQL query. Disabled unless B2MATCH_ALLOW_RAW=1. Rejects mutations, subscriptions, and queries that select messagesV2 or the notifications list unless ${SIDE_EFFECT_READS_ENV}=1.`,
|
|
261
290
|
inputSchema: z.object({
|
|
262
|
-
query: z.string(),
|
|
263
|
-
variables: z.record(z.string(), z.unknown()).default({}),
|
|
264
|
-
require_auth: z
|
|
291
|
+
query: z.string().describe("GraphQL query document; only query operations are allowed."),
|
|
292
|
+
variables: z.record(z.string(), z.unknown()).default({}).describe("GraphQL variables object."),
|
|
293
|
+
require_auth: z
|
|
294
|
+
.boolean()
|
|
295
|
+
.default(false)
|
|
296
|
+
.describe("true sends the stored token and fails if none is available.")
|
|
265
297
|
}).shape,
|
|
266
298
|
annotations: readOnlyAnnotations
|
|
267
299
|
}, async ({ query, variables, require_auth }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "b2match-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"mcpName": "io.github.barzoj/b2match-mcp",
|
|
5
5
|
"description": "Read-only MCP server for the B2match event and matchmaking platform",
|
|
6
6
|
"type": "module",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
],
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
46
|
+
"node": ">=22"
|
|
47
47
|
},
|
|
48
48
|
"bugs": {
|
|
49
49
|
"url": "https://github.com/barzoj/b2match-mcp/issues"
|