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 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 20 or newer. On macOS with Apple Silicon, the recommended install path is `npx`; no native build step is required for the auth storage path.
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 visible from the authenticated viewer account.",
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 authenticated participant status, activation, permissions, organisation pages, and opportunities for one event.",
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.boolean().default(false)
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 a participant profile without sending B2match's profile-view tracking mutation.",
73
- inputSchema: EventInput.extend({ participant_id: z.string() }).shape,
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 event organisation pages. Resolves the menu path automatically; pass path to override.",
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 an organisation/participation page. Resolves the menu path automatically; pass path to override.",
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 a session route. Provide session_id, or path to a session URL. Path alone is enough; session_id is not required when path is set.",
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: "List attendees for a session. Returns only the attendees connection, not the full session route. Provide session_id or path.",
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 authenticated viewer's event schedule. The PDF agenda URL is omitted unless include_pdf_link is true.",
153
- inputSchema: EventInput.extend({ include_pdf_link: z.boolean().default(false) }).shape,
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 authenticated meetings, optionally returning one status bucket. Unfiltered responses include a compact availability summary (merged free ranges per day) unless detail=true.",
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.enum(["accepted", "receivedInvitations", "sentInvitations", "canceled"]).optional(),
161
- detail: z.boolean().default(false)
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 authenticated availability. Default is merged free ranges per day; set detail=true for the full timeslot grid.",
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: "Read common free meeting slots for the viewer and selected participants. Does not book anything.",
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.array(z.string()).min(1).max(10),
178
- day: z.string().optional(),
179
- duration: z.string().optional(),
180
- is_long_term_networking: z.boolean().default(false)
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 authenticated recommended and newest participants to meet.",
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 authenticated conversations and last-message previews. This list does not mark conversations as read.",
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.boolean().default(false)
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.0",
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": ">=20"
46
+ "node": ">=22"
47
47
  },
48
48
  "bugs": {
49
49
  "url": "https://github.com/barzoj/b2match-mcp/issues"