@kajidog/connpass-mcp-server 0.3.0 → 0.4.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 +72 -149
- package/dist/index.d.ts +0 -2
- package/dist/index.js +4749 -162
- package/dist/index.js.map +1 -1
- package/dist/mcp-app.html +139 -0
- package/dist/stdio.d.ts +1 -0
- package/dist/stdio.js +2140 -0
- package/dist/stdio.js.map +1 -0
- package/package.json +18 -18
- package/dist/apps-sdk.d.ts +0 -8
- package/dist/apps-sdk.d.ts.map +0 -1
- package/dist/apps-sdk.js +0 -93
- package/dist/apps-sdk.js.map +0 -1
- package/dist/config.d.ts +0 -7
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -82
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/tools/events.d.ts +0 -74
- package/dist/tools/events.d.ts.map +0 -1
- package/dist/tools/events.js +0 -413
- package/dist/tools/events.js.map +0 -1
- package/dist/tools/formatting.d.ts +0 -74
- package/dist/tools/formatting.d.ts.map +0 -1
- package/dist/tools/formatting.js +0 -222
- package/dist/tools/formatting.js.map +0 -1
- package/dist/tools/groups.d.ts +0 -48
- package/dist/tools/groups.d.ts.map +0 -1
- package/dist/tools/groups.js +0 -106
- package/dist/tools/groups.js.map +0 -1
- package/dist/tools/index.d.ts +0 -54
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -21
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/shared.d.ts +0 -24
- package/dist/tools/shared.d.ts.map +0 -1
- package/dist/tools/shared.js +0 -113
- package/dist/tools/shared.js.map +0 -1
- package/dist/tools/users.d.ts +0 -51
- package/dist/tools/users.d.ts.map +0 -1
- package/dist/tools/users.js +0 -239
- package/dist/tools/users.js.map +0 -1
- package/dist/transports/http.d.ts +0 -17
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js +0 -185
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/sse.d.ts +0 -21
- package/dist/transports/sse.d.ts.map +0 -1
- package/dist/transports/sse.js +0 -161
- package/dist/transports/sse.js.map +0 -1
- package/dist/widgets/connpass-events.d.ts +0 -41
- package/dist/widgets/connpass-events.d.ts.map +0 -1
- package/dist/widgets/connpass-events.js +0 -68
- package/dist/widgets/connpass-events.js.map +0 -1
- package/dist/widgets/connpass-schedule.d.ts +0 -41
- package/dist/widgets/connpass-schedule.d.ts.map +0 -1
- package/dist/widgets/connpass-schedule.js +0 -68
- package/dist/widgets/connpass-schedule.js.map +0 -1
- package/dist/widgets/index.d.ts +0 -6
- package/dist/widgets/index.d.ts.map +0 -1
- package/dist/widgets/index.js +0 -39
- package/dist/widgets/index.js.map +0 -1
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,2140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../packages/mcp-core/dist/config-schema.js
|
|
4
|
+
function parseCliFromDefs(defs, argv) {
|
|
5
|
+
const config2 = {};
|
|
6
|
+
const flagMap = /* @__PURE__ */ new Map();
|
|
7
|
+
const negationMap = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
9
|
+
flagMap.set(def.cli, { key, def });
|
|
10
|
+
if (def.type === "boolean") {
|
|
11
|
+
const baseName = def.cli.replace(/^--/, "");
|
|
12
|
+
negationMap.set(`--no-${baseName}`, key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
const nextArg = argv[i + 1];
|
|
18
|
+
const negKey = negationMap.get(arg);
|
|
19
|
+
if (negKey !== void 0) {
|
|
20
|
+
config2[negKey] = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const entry = flagMap.get(arg);
|
|
24
|
+
if (!entry)
|
|
25
|
+
continue;
|
|
26
|
+
const { key, def } = entry;
|
|
27
|
+
switch (def.type) {
|
|
28
|
+
case "boolean":
|
|
29
|
+
config2[key] = true;
|
|
30
|
+
break;
|
|
31
|
+
case "string":
|
|
32
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
33
|
+
config2[key] = nextArg;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case "number":
|
|
38
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
39
|
+
const num = Number(nextArg);
|
|
40
|
+
if (Number.isFinite(num)) {
|
|
41
|
+
config2[key] = num;
|
|
42
|
+
}
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "string[]":
|
|
47
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
48
|
+
config2[key] = nextArg.split(",").map((s) => s.trim());
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return config2;
|
|
55
|
+
}
|
|
56
|
+
function parseEnvFromDefs(defs, env) {
|
|
57
|
+
const config2 = {};
|
|
58
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
59
|
+
if (!def.env)
|
|
60
|
+
continue;
|
|
61
|
+
const val = env[def.env];
|
|
62
|
+
if (val === void 0)
|
|
63
|
+
continue;
|
|
64
|
+
switch (def.type) {
|
|
65
|
+
case "boolean":
|
|
66
|
+
if (val === "true")
|
|
67
|
+
config2[key] = true;
|
|
68
|
+
else if (val === "false")
|
|
69
|
+
config2[key] = false;
|
|
70
|
+
break;
|
|
71
|
+
case "number": {
|
|
72
|
+
if (val === "")
|
|
73
|
+
break;
|
|
74
|
+
const num = Number(val);
|
|
75
|
+
if (Number.isFinite(num))
|
|
76
|
+
config2[key] = num;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "string":
|
|
80
|
+
if (val)
|
|
81
|
+
config2[key] = val;
|
|
82
|
+
break;
|
|
83
|
+
case "string[]":
|
|
84
|
+
if (val)
|
|
85
|
+
config2[key] = val.split(",").map((s) => s.trim());
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return config2;
|
|
90
|
+
}
|
|
91
|
+
function getDefaultsFromDefs(defs) {
|
|
92
|
+
const defaults = {};
|
|
93
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
94
|
+
if (def.default !== void 0) {
|
|
95
|
+
defaults[key] = def.default;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return defaults;
|
|
99
|
+
}
|
|
100
|
+
function filterUndefined(obj) {
|
|
101
|
+
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== void 0));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ../../packages/mcp-core/dist/config.js
|
|
105
|
+
var baseConfigDefs = {
|
|
106
|
+
httpMode: {
|
|
107
|
+
cli: "--http",
|
|
108
|
+
env: "MCP_HTTP_MODE",
|
|
109
|
+
description: "Enable HTTP server mode",
|
|
110
|
+
group: "Server Options",
|
|
111
|
+
type: "boolean",
|
|
112
|
+
default: false
|
|
113
|
+
},
|
|
114
|
+
httpPort: {
|
|
115
|
+
cli: "--port",
|
|
116
|
+
env: "MCP_HTTP_PORT",
|
|
117
|
+
description: "HTTP server port",
|
|
118
|
+
group: "Server Options",
|
|
119
|
+
type: "number",
|
|
120
|
+
default: 3e3,
|
|
121
|
+
valueName: "<port>"
|
|
122
|
+
},
|
|
123
|
+
httpHost: {
|
|
124
|
+
cli: "--host",
|
|
125
|
+
env: "MCP_HTTP_HOST",
|
|
126
|
+
description: "HTTP server host",
|
|
127
|
+
group: "Server Options",
|
|
128
|
+
type: "string",
|
|
129
|
+
default: "0.0.0.0",
|
|
130
|
+
valueName: "<host>"
|
|
131
|
+
},
|
|
132
|
+
allowedHosts: {
|
|
133
|
+
cli: "--allowed-hosts",
|
|
134
|
+
env: "MCP_ALLOWED_HOSTS",
|
|
135
|
+
description: "Comma-separated list of allowed hosts",
|
|
136
|
+
group: "Server Options",
|
|
137
|
+
type: "string[]",
|
|
138
|
+
default: ["localhost", "127.0.0.1", "[::1]"],
|
|
139
|
+
valueName: "<hosts>"
|
|
140
|
+
},
|
|
141
|
+
allowedOrigins: {
|
|
142
|
+
cli: "--allowed-origins",
|
|
143
|
+
env: "MCP_ALLOWED_ORIGINS",
|
|
144
|
+
description: "Comma-separated list of allowed origins",
|
|
145
|
+
group: "Server Options",
|
|
146
|
+
type: "string[]",
|
|
147
|
+
default: [
|
|
148
|
+
"http://localhost",
|
|
149
|
+
"http://127.0.0.1",
|
|
150
|
+
"https://localhost",
|
|
151
|
+
"https://127.0.0.1"
|
|
152
|
+
],
|
|
153
|
+
valueName: "<origins>"
|
|
154
|
+
},
|
|
155
|
+
apiKey: {
|
|
156
|
+
cli: "--api-key",
|
|
157
|
+
env: "MCP_API_KEY",
|
|
158
|
+
description: "Require matching API key via X-API-Key or Authorization: Bearer",
|
|
159
|
+
group: "Server Options",
|
|
160
|
+
type: "string",
|
|
161
|
+
valueName: "<key>"
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var defaultBaseConfig = getDefaultsFromDefs(baseConfigDefs);
|
|
165
|
+
|
|
166
|
+
// ../../packages/mcp-core/dist/http.js
|
|
167
|
+
import { randomUUID } from "crypto";
|
|
168
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
169
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
170
|
+
|
|
171
|
+
// ../../node_modules/.pnpm/hono@4.11.3/node_modules/hono/dist/request/constants.js
|
|
172
|
+
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
|
|
173
|
+
|
|
174
|
+
// ../../node_modules/.pnpm/hono@4.11.3/node_modules/hono/dist/utils/body.js
|
|
175
|
+
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
|
|
176
|
+
const { all = false, dot = false } = options;
|
|
177
|
+
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
|
|
178
|
+
const contentType = headers.get("Content-Type");
|
|
179
|
+
if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) {
|
|
180
|
+
return parseFormData(request, { all, dot });
|
|
181
|
+
}
|
|
182
|
+
return {};
|
|
183
|
+
};
|
|
184
|
+
async function parseFormData(request, options) {
|
|
185
|
+
const formData = await request.formData();
|
|
186
|
+
if (formData) {
|
|
187
|
+
return convertFormDataToBodyData(formData, options);
|
|
188
|
+
}
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
function convertFormDataToBodyData(formData, options) {
|
|
192
|
+
const form = /* @__PURE__ */ Object.create(null);
|
|
193
|
+
formData.forEach((value, key) => {
|
|
194
|
+
const shouldParseAllValues = options.all || key.endsWith("[]");
|
|
195
|
+
if (!shouldParseAllValues) {
|
|
196
|
+
form[key] = value;
|
|
197
|
+
} else {
|
|
198
|
+
handleParsingAllValues(form, key, value);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
if (options.dot) {
|
|
202
|
+
Object.entries(form).forEach(([key, value]) => {
|
|
203
|
+
const shouldParseDotValues = key.includes(".");
|
|
204
|
+
if (shouldParseDotValues) {
|
|
205
|
+
handleParsingNestedValues(form, key, value);
|
|
206
|
+
delete form[key];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return form;
|
|
211
|
+
}
|
|
212
|
+
var handleParsingAllValues = (form, key, value) => {
|
|
213
|
+
if (form[key] !== void 0) {
|
|
214
|
+
if (Array.isArray(form[key])) {
|
|
215
|
+
;
|
|
216
|
+
form[key].push(value);
|
|
217
|
+
} else {
|
|
218
|
+
form[key] = [form[key], value];
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
if (!key.endsWith("[]")) {
|
|
222
|
+
form[key] = value;
|
|
223
|
+
} else {
|
|
224
|
+
form[key] = [value];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var handleParsingNestedValues = (form, key, value) => {
|
|
229
|
+
let nestedForm = form;
|
|
230
|
+
const keys = key.split(".");
|
|
231
|
+
keys.forEach((key2, index) => {
|
|
232
|
+
if (index === keys.length - 1) {
|
|
233
|
+
nestedForm[key2] = value;
|
|
234
|
+
} else {
|
|
235
|
+
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
236
|
+
nestedForm[key2] = /* @__PURE__ */ Object.create(null);
|
|
237
|
+
}
|
|
238
|
+
nestedForm = nestedForm[key2];
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// ../../node_modules/.pnpm/hono@4.11.3/node_modules/hono/dist/utils/url.js
|
|
244
|
+
var tryDecode = (str, decoder) => {
|
|
245
|
+
try {
|
|
246
|
+
return decoder(str);
|
|
247
|
+
} catch {
|
|
248
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
|
|
249
|
+
try {
|
|
250
|
+
return decoder(match2);
|
|
251
|
+
} catch {
|
|
252
|
+
return match2;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var _decodeURI = (value) => {
|
|
258
|
+
if (!/[%+]/.test(value)) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
if (value.indexOf("+") !== -1) {
|
|
262
|
+
value = value.replace(/\+/g, " ");
|
|
263
|
+
}
|
|
264
|
+
return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
|
|
265
|
+
};
|
|
266
|
+
var _getQueryParam = (url, key, multiple) => {
|
|
267
|
+
let encoded;
|
|
268
|
+
if (!multiple && key && !/[%+]/.test(key)) {
|
|
269
|
+
let keyIndex2 = url.indexOf("?", 8);
|
|
270
|
+
if (keyIndex2 === -1) {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
if (!url.startsWith(key, keyIndex2 + 1)) {
|
|
274
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
275
|
+
}
|
|
276
|
+
while (keyIndex2 !== -1) {
|
|
277
|
+
const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
|
|
278
|
+
if (trailingKeyCode === 61) {
|
|
279
|
+
const valueIndex = keyIndex2 + key.length + 2;
|
|
280
|
+
const endIndex = url.indexOf("&", valueIndex);
|
|
281
|
+
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
|
|
282
|
+
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
286
|
+
}
|
|
287
|
+
encoded = /[%+]/.test(url);
|
|
288
|
+
if (!encoded) {
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const results = {};
|
|
293
|
+
encoded ??= /[%+]/.test(url);
|
|
294
|
+
let keyIndex = url.indexOf("?", 8);
|
|
295
|
+
while (keyIndex !== -1) {
|
|
296
|
+
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
|
|
297
|
+
let valueIndex = url.indexOf("=", keyIndex);
|
|
298
|
+
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
|
|
299
|
+
valueIndex = -1;
|
|
300
|
+
}
|
|
301
|
+
let name = url.slice(
|
|
302
|
+
keyIndex + 1,
|
|
303
|
+
valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex
|
|
304
|
+
);
|
|
305
|
+
if (encoded) {
|
|
306
|
+
name = _decodeURI(name);
|
|
307
|
+
}
|
|
308
|
+
keyIndex = nextKeyIndex;
|
|
309
|
+
if (name === "") {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let value;
|
|
313
|
+
if (valueIndex === -1) {
|
|
314
|
+
value = "";
|
|
315
|
+
} else {
|
|
316
|
+
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
|
|
317
|
+
if (encoded) {
|
|
318
|
+
value = _decodeURI(value);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (multiple) {
|
|
322
|
+
if (!(results[name] && Array.isArray(results[name]))) {
|
|
323
|
+
results[name] = [];
|
|
324
|
+
}
|
|
325
|
+
;
|
|
326
|
+
results[name].push(value);
|
|
327
|
+
} else {
|
|
328
|
+
results[name] ??= value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return key ? results[key] : results;
|
|
332
|
+
};
|
|
333
|
+
var getQueryParam = _getQueryParam;
|
|
334
|
+
var getQueryParams = (url, key) => {
|
|
335
|
+
return _getQueryParam(url, key, true);
|
|
336
|
+
};
|
|
337
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
338
|
+
|
|
339
|
+
// ../../node_modules/.pnpm/hono@4.11.3/node_modules/hono/dist/request.js
|
|
340
|
+
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
|
|
341
|
+
var HonoRequest = class {
|
|
342
|
+
/**
|
|
343
|
+
* `.raw` can get the raw Request object.
|
|
344
|
+
*
|
|
345
|
+
* @see {@link https://hono.dev/docs/api/request#raw}
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* // For Cloudflare Workers
|
|
350
|
+
* app.post('/', async (c) => {
|
|
351
|
+
* const metadata = c.req.raw.cf?.hostMetadata?
|
|
352
|
+
* ...
|
|
353
|
+
* })
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
raw;
|
|
357
|
+
#validatedData;
|
|
358
|
+
// Short name of validatedData
|
|
359
|
+
#matchResult;
|
|
360
|
+
routeIndex = 0;
|
|
361
|
+
/**
|
|
362
|
+
* `.path` can get the pathname of the request.
|
|
363
|
+
*
|
|
364
|
+
* @see {@link https://hono.dev/docs/api/request#path}
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* app.get('/about/me', (c) => {
|
|
369
|
+
* const pathname = c.req.path // `/about/me`
|
|
370
|
+
* })
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
path;
|
|
374
|
+
bodyCache = {};
|
|
375
|
+
constructor(request, path = "/", matchResult = [[]]) {
|
|
376
|
+
this.raw = request;
|
|
377
|
+
this.path = path;
|
|
378
|
+
this.#matchResult = matchResult;
|
|
379
|
+
this.#validatedData = {};
|
|
380
|
+
}
|
|
381
|
+
param(key) {
|
|
382
|
+
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
|
|
383
|
+
}
|
|
384
|
+
#getDecodedParam(key) {
|
|
385
|
+
const paramKey = this.#matchResult[0][this.routeIndex][1][key];
|
|
386
|
+
const param = this.#getParamValue(paramKey);
|
|
387
|
+
return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param;
|
|
388
|
+
}
|
|
389
|
+
#getAllDecodedParams() {
|
|
390
|
+
const decoded = {};
|
|
391
|
+
const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
|
|
392
|
+
for (const key of keys) {
|
|
393
|
+
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
|
|
394
|
+
if (value !== void 0) {
|
|
395
|
+
decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return decoded;
|
|
399
|
+
}
|
|
400
|
+
#getParamValue(paramKey) {
|
|
401
|
+
return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
|
|
402
|
+
}
|
|
403
|
+
query(key) {
|
|
404
|
+
return getQueryParam(this.url, key);
|
|
405
|
+
}
|
|
406
|
+
queries(key) {
|
|
407
|
+
return getQueryParams(this.url, key);
|
|
408
|
+
}
|
|
409
|
+
header(name) {
|
|
410
|
+
if (name) {
|
|
411
|
+
return this.raw.headers.get(name) ?? void 0;
|
|
412
|
+
}
|
|
413
|
+
const headerData = {};
|
|
414
|
+
this.raw.headers.forEach((value, key) => {
|
|
415
|
+
headerData[key] = value;
|
|
416
|
+
});
|
|
417
|
+
return headerData;
|
|
418
|
+
}
|
|
419
|
+
async parseBody(options) {
|
|
420
|
+
return this.bodyCache.parsedBody ??= await parseBody(this, options);
|
|
421
|
+
}
|
|
422
|
+
#cachedBody = (key) => {
|
|
423
|
+
const { bodyCache, raw } = this;
|
|
424
|
+
const cachedBody = bodyCache[key];
|
|
425
|
+
if (cachedBody) {
|
|
426
|
+
return cachedBody;
|
|
427
|
+
}
|
|
428
|
+
const anyCachedKey = Object.keys(bodyCache)[0];
|
|
429
|
+
if (anyCachedKey) {
|
|
430
|
+
return bodyCache[anyCachedKey].then((body) => {
|
|
431
|
+
if (anyCachedKey === "json") {
|
|
432
|
+
body = JSON.stringify(body);
|
|
433
|
+
}
|
|
434
|
+
return new Response(body)[key]();
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return bodyCache[key] = raw[key]();
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* `.json()` can parse Request body of type `application/json`
|
|
441
|
+
*
|
|
442
|
+
* @see {@link https://hono.dev/docs/api/request#json}
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```ts
|
|
446
|
+
* app.post('/entry', async (c) => {
|
|
447
|
+
* const body = await c.req.json()
|
|
448
|
+
* })
|
|
449
|
+
* ```
|
|
450
|
+
*/
|
|
451
|
+
json() {
|
|
452
|
+
return this.#cachedBody("text").then((text) => JSON.parse(text));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* `.text()` can parse Request body of type `text/plain`
|
|
456
|
+
*
|
|
457
|
+
* @see {@link https://hono.dev/docs/api/request#text}
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```ts
|
|
461
|
+
* app.post('/entry', async (c) => {
|
|
462
|
+
* const body = await c.req.text()
|
|
463
|
+
* })
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
text() {
|
|
467
|
+
return this.#cachedBody("text");
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* `.arrayBuffer()` parse Request body as an `ArrayBuffer`
|
|
471
|
+
*
|
|
472
|
+
* @see {@link https://hono.dev/docs/api/request#arraybuffer}
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* app.post('/entry', async (c) => {
|
|
477
|
+
* const body = await c.req.arrayBuffer()
|
|
478
|
+
* })
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
arrayBuffer() {
|
|
482
|
+
return this.#cachedBody("arrayBuffer");
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Parses the request body as a `Blob`.
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* app.post('/entry', async (c) => {
|
|
489
|
+
* const body = await c.req.blob();
|
|
490
|
+
* });
|
|
491
|
+
* ```
|
|
492
|
+
* @see https://hono.dev/docs/api/request#blob
|
|
493
|
+
*/
|
|
494
|
+
blob() {
|
|
495
|
+
return this.#cachedBody("blob");
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Parses the request body as `FormData`.
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* app.post('/entry', async (c) => {
|
|
502
|
+
* const body = await c.req.formData();
|
|
503
|
+
* });
|
|
504
|
+
* ```
|
|
505
|
+
* @see https://hono.dev/docs/api/request#formdata
|
|
506
|
+
*/
|
|
507
|
+
formData() {
|
|
508
|
+
return this.#cachedBody("formData");
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Adds validated data to the request.
|
|
512
|
+
*
|
|
513
|
+
* @param target - The target of the validation.
|
|
514
|
+
* @param data - The validated data to add.
|
|
515
|
+
*/
|
|
516
|
+
addValidatedData(target, data) {
|
|
517
|
+
this.#validatedData[target] = data;
|
|
518
|
+
}
|
|
519
|
+
valid(target) {
|
|
520
|
+
return this.#validatedData[target];
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* `.url()` can get the request url strings.
|
|
524
|
+
*
|
|
525
|
+
* @see {@link https://hono.dev/docs/api/request#url}
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* app.get('/about/me', (c) => {
|
|
530
|
+
* const url = c.req.url // `http://localhost:8787/about/me`
|
|
531
|
+
* ...
|
|
532
|
+
* })
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
get url() {
|
|
536
|
+
return this.raw.url;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* `.method()` can get the method name of the request.
|
|
540
|
+
*
|
|
541
|
+
* @see {@link https://hono.dev/docs/api/request#method}
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* ```ts
|
|
545
|
+
* app.get('/about/me', (c) => {
|
|
546
|
+
* const method = c.req.method // `GET`
|
|
547
|
+
* })
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
get method() {
|
|
551
|
+
return this.raw.method;
|
|
552
|
+
}
|
|
553
|
+
get [GET_MATCH_RESULT]() {
|
|
554
|
+
return this.#matchResult;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* `.matchedRoutes()` can return a matched route in the handler
|
|
558
|
+
*
|
|
559
|
+
* @deprecated
|
|
560
|
+
*
|
|
561
|
+
* Use matchedRoutes helper defined in "hono/route" instead.
|
|
562
|
+
*
|
|
563
|
+
* @see {@link https://hono.dev/docs/api/request#matchedroutes}
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```ts
|
|
567
|
+
* app.use('*', async function logger(c, next) {
|
|
568
|
+
* await next()
|
|
569
|
+
* c.req.matchedRoutes.forEach(({ handler, method, path }, i) => {
|
|
570
|
+
* const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]')
|
|
571
|
+
* console.log(
|
|
572
|
+
* method,
|
|
573
|
+
* ' ',
|
|
574
|
+
* path,
|
|
575
|
+
* ' '.repeat(Math.max(10 - path.length, 0)),
|
|
576
|
+
* name,
|
|
577
|
+
* i === c.req.routeIndex ? '<- respond from here' : ''
|
|
578
|
+
* )
|
|
579
|
+
* })
|
|
580
|
+
* })
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
get matchedRoutes() {
|
|
584
|
+
return this.#matchResult[0].map(([[, route]]) => route);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* `routePath()` can retrieve the path registered within the handler
|
|
588
|
+
*
|
|
589
|
+
* @deprecated
|
|
590
|
+
*
|
|
591
|
+
* Use routePath helper defined in "hono/route" instead.
|
|
592
|
+
*
|
|
593
|
+
* @see {@link https://hono.dev/docs/api/request#routepath}
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```ts
|
|
597
|
+
* app.get('/posts/:id', (c) => {
|
|
598
|
+
* return c.json({ path: c.req.routePath })
|
|
599
|
+
* })
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
get routePath() {
|
|
603
|
+
return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// ../../node_modules/.pnpm/hono@4.11.3/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
608
|
+
var regExpMetaChars = new Set(".\\+*[^]$()");
|
|
609
|
+
|
|
610
|
+
// ../../packages/mcp-core/dist/stdio.js
|
|
611
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
612
|
+
async function connectStdio(server2) {
|
|
613
|
+
await server2.connect(new StdioServerTransport());
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/server.ts
|
|
617
|
+
import { ConnpassClient } from "@kajidog/connpass-api-client";
|
|
618
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
619
|
+
|
|
620
|
+
// src/config.ts
|
|
621
|
+
var connpassConfigDefs = {
|
|
622
|
+
connpassApiKey: {
|
|
623
|
+
cli: "--connpass-api-key",
|
|
624
|
+
env: "CONNPASS_API_KEY",
|
|
625
|
+
description: "Connpass API key",
|
|
626
|
+
group: "Connpass Configuration",
|
|
627
|
+
type: "string",
|
|
628
|
+
valueName: "<key>"
|
|
629
|
+
},
|
|
630
|
+
defaultUserId: {
|
|
631
|
+
cli: "--default-user-id",
|
|
632
|
+
env: "CONNPASS_DEFAULT_USER_ID",
|
|
633
|
+
description: "Default Connpass user ID for schedule search",
|
|
634
|
+
group: "Connpass Configuration",
|
|
635
|
+
type: "number",
|
|
636
|
+
valueName: "<id>"
|
|
637
|
+
},
|
|
638
|
+
rateLimitEnabled: {
|
|
639
|
+
cli: "--rate-limit",
|
|
640
|
+
env: "CONNPASS_RATE_LIMIT_ENABLED",
|
|
641
|
+
description: "Enable API rate limiting",
|
|
642
|
+
group: "Connpass Configuration",
|
|
643
|
+
type: "boolean",
|
|
644
|
+
default: true
|
|
645
|
+
},
|
|
646
|
+
rateLimitDelayMs: {
|
|
647
|
+
cli: "--rate-limit-delay",
|
|
648
|
+
env: "CONNPASS_RATE_LIMIT_DELAY_MS",
|
|
649
|
+
description: "Rate limit delay in milliseconds",
|
|
650
|
+
group: "Connpass Configuration",
|
|
651
|
+
type: "number",
|
|
652
|
+
default: 1e3,
|
|
653
|
+
valueName: "<ms>"
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
var allConfigDefs = {
|
|
657
|
+
...connpassConfigDefs,
|
|
658
|
+
...baseConfigDefs
|
|
659
|
+
};
|
|
660
|
+
function createDefaultConfig() {
|
|
661
|
+
const schemaDefs = getDefaultsFromDefs(allConfigDefs);
|
|
662
|
+
return schemaDefs;
|
|
663
|
+
}
|
|
664
|
+
function getConfig(argv, env) {
|
|
665
|
+
const cliConfig = parseCliFromDefs(
|
|
666
|
+
allConfigDefs,
|
|
667
|
+
argv ?? process.argv.slice(2)
|
|
668
|
+
);
|
|
669
|
+
const envConfig = parseEnvFromDefs(
|
|
670
|
+
allConfigDefs,
|
|
671
|
+
env ?? process.env
|
|
672
|
+
);
|
|
673
|
+
const defaultConfig = createDefaultConfig();
|
|
674
|
+
const merged = {
|
|
675
|
+
...defaultConfig,
|
|
676
|
+
...filterUndefined(envConfig),
|
|
677
|
+
...filterUndefined(cliConfig)
|
|
678
|
+
};
|
|
679
|
+
return merged;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/tools/events.ts
|
|
683
|
+
import { z as z2 } from "zod";
|
|
684
|
+
|
|
685
|
+
// src/tools/prefectures.ts
|
|
686
|
+
import {
|
|
687
|
+
getAllPrefectures,
|
|
688
|
+
normalizePrefecture
|
|
689
|
+
} from "@kajidog/connpass-api-client";
|
|
690
|
+
import { z } from "zod";
|
|
691
|
+
|
|
692
|
+
// src/tools/utils/registration.ts
|
|
693
|
+
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
694
|
+
function registerAppToolIfEnabled(server2, name, ...args) {
|
|
695
|
+
const [config2, cb] = args;
|
|
696
|
+
const normalizedConfig = {
|
|
697
|
+
...config2,
|
|
698
|
+
_meta: config2._meta && typeof config2._meta === "object" ? config2._meta : {}
|
|
699
|
+
};
|
|
700
|
+
registerAppTool(server2, name, normalizedConfig, cb);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/tools/prefectures.ts
|
|
704
|
+
var EmptyInputSchema = z.object({});
|
|
705
|
+
function buildPrefectureListResult(invalid = []) {
|
|
706
|
+
const prefectures = getAllPrefectures();
|
|
707
|
+
const payload = {
|
|
708
|
+
invalid,
|
|
709
|
+
prefectures
|
|
710
|
+
};
|
|
711
|
+
const invalidText = invalid.length > 0 ? `\u6307\u5B9A\u3055\u308C\u305F\u90FD\u9053\u5E9C\u770C\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${invalid.join(", ")}
|
|
712
|
+
` : "";
|
|
713
|
+
return {
|
|
714
|
+
content: [
|
|
715
|
+
{
|
|
716
|
+
type: "text",
|
|
717
|
+
text: `${invalidText}\u5229\u7528\u53EF\u80FD\u306A\u90FD\u9053\u5E9C\u770C\u4E00\u89A7: ${prefectures.map((item) => `${item.name} (${item.code})`).join(", ")}`
|
|
718
|
+
}
|
|
719
|
+
],
|
|
720
|
+
structuredContent: {
|
|
721
|
+
kind: "prefectures",
|
|
722
|
+
data: payload
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function resolvePrefectureInputs(values) {
|
|
727
|
+
if (!values) return {};
|
|
728
|
+
const inputs = Array.isArray(values) ? values : [values];
|
|
729
|
+
const prefectures = [];
|
|
730
|
+
const invalid = [];
|
|
731
|
+
for (const value of inputs) {
|
|
732
|
+
const normalized = normalizePrefecture(value);
|
|
733
|
+
if (!normalized) {
|
|
734
|
+
invalid.push(value);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
prefectures.push(normalized);
|
|
738
|
+
}
|
|
739
|
+
if (invalid.length > 0) {
|
|
740
|
+
return { response: buildPrefectureListResult(invalid) };
|
|
741
|
+
}
|
|
742
|
+
return { prefectures };
|
|
743
|
+
}
|
|
744
|
+
function registerPrefectureTools(deps) {
|
|
745
|
+
const { server: server2 } = deps;
|
|
746
|
+
registerAppToolIfEnabled(
|
|
747
|
+
server2,
|
|
748
|
+
"list_prefectures",
|
|
749
|
+
{
|
|
750
|
+
title: "List Prefectures",
|
|
751
|
+
description: "List supported prefectures and region codes for filtering",
|
|
752
|
+
inputSchema: EmptyInputSchema
|
|
753
|
+
},
|
|
754
|
+
async () => buildPrefectureListResult()
|
|
755
|
+
);
|
|
756
|
+
registerAppToolIfEnabled(
|
|
757
|
+
server2,
|
|
758
|
+
"_get_prefectures",
|
|
759
|
+
{
|
|
760
|
+
title: "Get Prefectures (UI)",
|
|
761
|
+
description: "Internal: list supported prefectures for the UI",
|
|
762
|
+
inputSchema: EmptyInputSchema,
|
|
763
|
+
_meta: {
|
|
764
|
+
ui: {
|
|
765
|
+
visibility: ["app"]
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
async () => buildPrefectureListResult()
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/tools/utils/formatting.ts
|
|
774
|
+
var HTML_ENTITY_MAP = {
|
|
775
|
+
" ": " ",
|
|
776
|
+
"&": "&",
|
|
777
|
+
"<": "<",
|
|
778
|
+
">": ">",
|
|
779
|
+
""": '"',
|
|
780
|
+
""": '"',
|
|
781
|
+
"'": "'",
|
|
782
|
+
"'": "'",
|
|
783
|
+
"`": "`"
|
|
784
|
+
};
|
|
785
|
+
var FORMAT_PRESETS = {
|
|
786
|
+
default: {
|
|
787
|
+
descriptionLimit: 0,
|
|
788
|
+
catchPhraseLimit: void 0
|
|
789
|
+
},
|
|
790
|
+
detailed: {
|
|
791
|
+
descriptionLimit: 200,
|
|
792
|
+
catchPhraseLimit: void 0
|
|
793
|
+
},
|
|
794
|
+
full: {
|
|
795
|
+
descriptionLimit: void 0,
|
|
796
|
+
catchPhraseLimit: void 0
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
function compactDateLabel(isoString) {
|
|
800
|
+
if (!isoString) return void 0;
|
|
801
|
+
const date = new Date(isoString);
|
|
802
|
+
if (Number.isNaN(date.getTime())) return void 0;
|
|
803
|
+
const year = date.getFullYear();
|
|
804
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
805
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
806
|
+
return `${year}-${month}-${day}`;
|
|
807
|
+
}
|
|
808
|
+
function summarizeEventLine(event) {
|
|
809
|
+
const date = compactDateLabel(event.schedule.start);
|
|
810
|
+
const place = event.location?.place || event.location?.address;
|
|
811
|
+
const { accepted, waiting, limit } = event.participants;
|
|
812
|
+
const capacityParts = [`${accepted}\u4EBA\u53C2\u52A0`];
|
|
813
|
+
if (waiting > 0) capacityParts.push(`\u5F85\u3061${waiting}`);
|
|
814
|
+
if (typeof limit === "number") capacityParts.push(`\u5B9A\u54E1${limit}`);
|
|
815
|
+
const capacity = capacityParts.join("/");
|
|
816
|
+
const fragments = [
|
|
817
|
+
`[id:${event.id}]`,
|
|
818
|
+
date,
|
|
819
|
+
event.title,
|
|
820
|
+
place ? `@ ${place}` : void 0,
|
|
821
|
+
`(${capacity})`
|
|
822
|
+
].filter(Boolean);
|
|
823
|
+
return `- ${fragments.join(" ")}`;
|
|
824
|
+
}
|
|
825
|
+
function summarizeGroupLine(group) {
|
|
826
|
+
const place = [group.prefecture, group.place].filter(Boolean).join(" ");
|
|
827
|
+
const fragments = [
|
|
828
|
+
group.title?.trim(),
|
|
829
|
+
typeof group.id === "number" ? `id:${group.id}` : void 0,
|
|
830
|
+
place || void 0
|
|
831
|
+
].filter(Boolean);
|
|
832
|
+
return `- ${fragments.join(" / ")}`;
|
|
833
|
+
}
|
|
834
|
+
function summarizeUserLine(user) {
|
|
835
|
+
const fragments = [
|
|
836
|
+
user.nickname?.trim(),
|
|
837
|
+
typeof user.id === "number" ? `id:${user.id}` : void 0,
|
|
838
|
+
user.displayName?.trim() ? `name:${user.displayName.trim()}` : void 0
|
|
839
|
+
].filter(Boolean);
|
|
840
|
+
return `- ${fragments.join(" / ")}`;
|
|
841
|
+
}
|
|
842
|
+
function truncateText(text, limit) {
|
|
843
|
+
if (!text) return "";
|
|
844
|
+
if (text.length <= limit) return text;
|
|
845
|
+
if (limit <= 3) return text.slice(0, limit);
|
|
846
|
+
return `${text.slice(0, limit - 3).trimEnd()}...`;
|
|
847
|
+
}
|
|
848
|
+
function decodeHtmlEntities(input) {
|
|
849
|
+
return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (entity) => {
|
|
850
|
+
const mapped = HTML_ENTITY_MAP[entity];
|
|
851
|
+
if (mapped) return mapped;
|
|
852
|
+
const numericMatch = entity.match(/^&#(x?[0-9a-fA-F]+);$/);
|
|
853
|
+
if (!numericMatch) return entity;
|
|
854
|
+
const value = numericMatch[1];
|
|
855
|
+
const codePoint = value.startsWith("x") || value.startsWith("X") ? Number.parseInt(value.slice(1), 16) : Number.parseInt(value, 10);
|
|
856
|
+
if (!Number.isFinite(codePoint)) return entity;
|
|
857
|
+
try {
|
|
858
|
+
return String.fromCodePoint(codePoint);
|
|
859
|
+
} catch {
|
|
860
|
+
return entity;
|
|
861
|
+
}
|
|
862
|
+
}).replace(/\u00a0/gi, " ");
|
|
863
|
+
}
|
|
864
|
+
function stripHtml(input) {
|
|
865
|
+
if (!input) return "";
|
|
866
|
+
const withoutScripts = input.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
867
|
+
const withoutStyles = withoutScripts.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
868
|
+
const withCellBrNormalized = withoutStyles.replace(
|
|
869
|
+
/(<(?:td|th)\b[^>]*>)([\s\S]*?)(<\/(?:td|th)>)/gi,
|
|
870
|
+
(_, open, content, close) => `${open}${content.replace(/<br\s*\/?\s*>/gi, " ")}${close}`
|
|
871
|
+
);
|
|
872
|
+
const normalizedTableCells = withCellBrNormalized.replace(/<\/(td|th)>\s*<(td|th)/gi, "</$1> <$2").replace(/<\/(tr)>\s*<tr/gi, "</$1>\n<tr");
|
|
873
|
+
const withLineBreaks = normalizedTableCells.replace(/<br\s*\/?\s*>/gi, "\n").replace(/<\/(p|div|section|article|header|footer|li)>/gi, "\n").replace(/<\/(td|th)>/gi, " ").replace(/<li[^>]*>/gi, "- ").replace(/<h[1-6]\b[^>]*>/gi, "\n## ").replace(/<\/(h[1-6]|tr)>/gi, "\n");
|
|
874
|
+
const withoutTags = withLineBreaks.replace(/<[^>]+>/g, "");
|
|
875
|
+
return decodeHtmlEntities(withoutTags);
|
|
876
|
+
}
|
|
877
|
+
function sanitizeRichText(input) {
|
|
878
|
+
if (!input) return "";
|
|
879
|
+
const stripped = stripHtml(input);
|
|
880
|
+
const normalizedWhitespace = stripped.replace(/\r/g, "\n").split(/\n+/).map(
|
|
881
|
+
(line) => line.split(/\t+/).map((cell) => cell.trim()).filter(Boolean).join(" ")
|
|
882
|
+
).filter(Boolean).join("\n");
|
|
883
|
+
return normalizedWhitespace.replace(/ {2,}/g, " ").replace(/ *\t */g, " ").trim();
|
|
884
|
+
}
|
|
885
|
+
function formatPresentation(presentation, descriptionLimit) {
|
|
886
|
+
const summary = sanitizeRichText(presentation.description);
|
|
887
|
+
const title = String(presentation.title ?? "").trim();
|
|
888
|
+
const speaker = String(presentation.speakerName ?? "").trim();
|
|
889
|
+
const formatted = {
|
|
890
|
+
id: presentation.id,
|
|
891
|
+
title: title || "Untitled presentation",
|
|
892
|
+
speaker: speaker || "Speaker unknown",
|
|
893
|
+
order: presentation.order,
|
|
894
|
+
updatedAt: presentation.updatedAt
|
|
895
|
+
};
|
|
896
|
+
const processedSummary = typeof descriptionLimit === "number" && descriptionLimit > 0 ? truncateText(summary, descriptionLimit) : summary;
|
|
897
|
+
if (processedSummary) {
|
|
898
|
+
formatted.summary = processedSummary;
|
|
899
|
+
}
|
|
900
|
+
const links = {
|
|
901
|
+
url: presentation.url,
|
|
902
|
+
slideshare: presentation.slideshareUrl,
|
|
903
|
+
youtube: presentation.youtubeUrl,
|
|
904
|
+
twitter: presentation.twitterUrl
|
|
905
|
+
};
|
|
906
|
+
if (links.url || links.slideshare || links.youtube || links.twitter) {
|
|
907
|
+
formatted.links = links;
|
|
908
|
+
}
|
|
909
|
+
return formatted;
|
|
910
|
+
}
|
|
911
|
+
function formatPresentationsResponse(response, options) {
|
|
912
|
+
const descriptionLimit = options?.presentationDescriptionLimit;
|
|
913
|
+
return {
|
|
914
|
+
returned: response.presentationsReturned,
|
|
915
|
+
presentations: response.presentations.map(
|
|
916
|
+
(presentation) => formatPresentation(presentation, descriptionLimit)
|
|
917
|
+
)
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function formatEvent(event, options) {
|
|
921
|
+
const descriptionLimit = options?.descriptionLimit;
|
|
922
|
+
const catchPhraseLimit = options?.catchPhraseLimit;
|
|
923
|
+
const presentationDescriptionLimit = options?.presentationDescriptionLimit;
|
|
924
|
+
const catchPhrase = sanitizeRichText(event.catchPhrase);
|
|
925
|
+
const description = sanitizeRichText(event.description);
|
|
926
|
+
const participants = {
|
|
927
|
+
accepted: event.participantCount,
|
|
928
|
+
waiting: event.waitingCount
|
|
929
|
+
};
|
|
930
|
+
if (typeof event.limit === "number") {
|
|
931
|
+
participants.limit = event.limit;
|
|
932
|
+
}
|
|
933
|
+
const formatted = {
|
|
934
|
+
id: event.id,
|
|
935
|
+
title: event.title.trim(),
|
|
936
|
+
url: event.url,
|
|
937
|
+
schedule: {
|
|
938
|
+
start: event.startedAt,
|
|
939
|
+
end: event.endedAt
|
|
940
|
+
},
|
|
941
|
+
owner: {
|
|
942
|
+
nickname: event.ownerNickname,
|
|
943
|
+
displayName: event.ownerDisplayName
|
|
944
|
+
},
|
|
945
|
+
participants,
|
|
946
|
+
updatedAt: event.updatedAt
|
|
947
|
+
};
|
|
948
|
+
if (event.hashTag) formatted.hashTag = event.hashTag;
|
|
949
|
+
if (event.imageUrl) formatted.imageUrl = event.imageUrl;
|
|
950
|
+
const processedCatchPhrase = typeof catchPhraseLimit === "number" && catchPhraseLimit > 0 ? truncateText(catchPhrase, catchPhraseLimit) : catchPhrase;
|
|
951
|
+
if (processedCatchPhrase) formatted.catchPhrase = processedCatchPhrase;
|
|
952
|
+
const processedDescription = typeof descriptionLimit === "number" ? descriptionLimit > 0 ? truncateText(description, descriptionLimit) : "" : description;
|
|
953
|
+
if (processedDescription) formatted.summary = processedDescription;
|
|
954
|
+
if (event.place || event.address) {
|
|
955
|
+
const location = {};
|
|
956
|
+
if (event.place) location.place = event.place;
|
|
957
|
+
if (event.address) location.address = event.address;
|
|
958
|
+
if (Object.keys(location).length > 0) formatted.location = location;
|
|
959
|
+
}
|
|
960
|
+
if (event.groupId || event.groupTitle || event.groupUrl) {
|
|
961
|
+
const group = {};
|
|
962
|
+
if (typeof event.groupId === "number") group.id = event.groupId;
|
|
963
|
+
if (event.groupTitle) group.title = event.groupTitle;
|
|
964
|
+
if (event.groupUrl) group.url = event.groupUrl;
|
|
965
|
+
if (Object.keys(group).length > 0) formatted.group = group;
|
|
966
|
+
}
|
|
967
|
+
const eventWithPresentations = event;
|
|
968
|
+
if (eventWithPresentations.presentations?.length) {
|
|
969
|
+
formatted.presentations = eventWithPresentations.presentations.map(
|
|
970
|
+
(presentation) => formatPresentation(presentation, presentationDescriptionLimit)
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return formatted;
|
|
974
|
+
}
|
|
975
|
+
function formatEventsResponse(response, options) {
|
|
976
|
+
return {
|
|
977
|
+
returned: response.eventsReturned,
|
|
978
|
+
available: response.eventsAvailable,
|
|
979
|
+
start: response.eventsStart,
|
|
980
|
+
events: response.events.map((event) => formatEvent(event, options))
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function summarizeEventBlock(event) {
|
|
984
|
+
const headline = summarizeEventLine(event);
|
|
985
|
+
const extras = [];
|
|
986
|
+
if (event.catchPhrase) extras.push(` ${event.catchPhrase}`);
|
|
987
|
+
if (event.group?.title) extras.push(` group: ${event.group.title}`);
|
|
988
|
+
if (extras.length === 0) return headline;
|
|
989
|
+
return [headline, ...extras].join("\n");
|
|
990
|
+
}
|
|
991
|
+
function summarizeEventsResponse(response, label = "events", options) {
|
|
992
|
+
const lines = [
|
|
993
|
+
`${label}: ${response.returned} returned / ${response.available} available`
|
|
994
|
+
];
|
|
995
|
+
if (options?.searchSessionId) {
|
|
996
|
+
lines.push(`searchSessionId: ${options.searchSessionId}`);
|
|
997
|
+
}
|
|
998
|
+
lines.push(...response.events.map(summarizeEventBlock));
|
|
999
|
+
return lines.join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
function summarizeGroupsResponse(response) {
|
|
1002
|
+
const lines = [
|
|
1003
|
+
`groups: ${response.groupsReturned} returned / ${response.groupsAvailable} available`,
|
|
1004
|
+
...response.groups.slice(0, 5).map(summarizeGroupLine)
|
|
1005
|
+
];
|
|
1006
|
+
if (response.groups.length > 5) {
|
|
1007
|
+
lines.push(`- ...and ${response.groups.length - 5} more`);
|
|
1008
|
+
}
|
|
1009
|
+
return lines.join("\n");
|
|
1010
|
+
}
|
|
1011
|
+
function summarizeUsersResponse(response) {
|
|
1012
|
+
const lines = [
|
|
1013
|
+
`users: ${response.usersReturned} returned / ${response.usersAvailable} available`,
|
|
1014
|
+
...response.users.slice(0, 5).map(summarizeUserLine)
|
|
1015
|
+
];
|
|
1016
|
+
if (response.users.length > 5) {
|
|
1017
|
+
lines.push(`- ...and ${response.users.length - 5} more`);
|
|
1018
|
+
}
|
|
1019
|
+
return lines.join("\n");
|
|
1020
|
+
}
|
|
1021
|
+
function formatEventList(events, options) {
|
|
1022
|
+
return events.map((event) => formatEvent(event, options));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/tools/utils/resource.ts
|
|
1026
|
+
import { readFileSync } from "fs";
|
|
1027
|
+
import { dirname, join } from "path";
|
|
1028
|
+
import { fileURLToPath } from "url";
|
|
1029
|
+
import {
|
|
1030
|
+
RESOURCE_MIME_TYPE,
|
|
1031
|
+
registerAppResource
|
|
1032
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
1033
|
+
var __dirname = typeof import.meta.dirname === "string" ? import.meta.dirname : dirname(fileURLToPath(import.meta.url));
|
|
1034
|
+
var connpassResourceUri = "ui://connpass/app.html";
|
|
1035
|
+
function loadConnpassHtml() {
|
|
1036
|
+
try {
|
|
1037
|
+
const htmlPath = join(__dirname, "mcp-app.html");
|
|
1038
|
+
return readFileSync(htmlPath, "utf-8");
|
|
1039
|
+
} catch {
|
|
1040
|
+
try {
|
|
1041
|
+
const htmlPath = join(
|
|
1042
|
+
__dirname,
|
|
1043
|
+
"..",
|
|
1044
|
+
"..",
|
|
1045
|
+
"node_modules",
|
|
1046
|
+
"@kajidog",
|
|
1047
|
+
"connpass-ui",
|
|
1048
|
+
"dist",
|
|
1049
|
+
"mcp-app.html"
|
|
1050
|
+
);
|
|
1051
|
+
return readFileSync(htmlPath, "utf-8");
|
|
1052
|
+
} catch {
|
|
1053
|
+
console.error(
|
|
1054
|
+
"Warning: connpass-ui HTML not found. Please build @kajidog/connpass-ui first."
|
|
1055
|
+
);
|
|
1056
|
+
return "<html><body><p>Connpass UI not available. Please build @kajidog/connpass-ui.</p></body></html>";
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
var connpassHtml = loadConnpassHtml();
|
|
1061
|
+
function registerConnpassResource(deps) {
|
|
1062
|
+
const { server: server2 } = deps;
|
|
1063
|
+
registerAppResource(
|
|
1064
|
+
server2,
|
|
1065
|
+
"Connpass App",
|
|
1066
|
+
connpassResourceUri,
|
|
1067
|
+
{
|
|
1068
|
+
description: "Interactive Connpass event browser",
|
|
1069
|
+
mimeType: RESOURCE_MIME_TYPE
|
|
1070
|
+
},
|
|
1071
|
+
async () => ({
|
|
1072
|
+
contents: [
|
|
1073
|
+
{
|
|
1074
|
+
uri: connpassResourceUri,
|
|
1075
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
1076
|
+
text: connpassHtml,
|
|
1077
|
+
_meta: {
|
|
1078
|
+
ui: {
|
|
1079
|
+
csp: {
|
|
1080
|
+
resourceDomains: [
|
|
1081
|
+
"https://connpass.com",
|
|
1082
|
+
"https://media.connpass.com"
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
})
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/tools/utils/shared.ts
|
|
1094
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
1095
|
+
var EVENT_SORT_KEYS = [
|
|
1096
|
+
"start-date-asc",
|
|
1097
|
+
"start-date-desc",
|
|
1098
|
+
"newly-added"
|
|
1099
|
+
];
|
|
1100
|
+
var EVENT_SORT_MAP = {
|
|
1101
|
+
"start-date-asc": 1,
|
|
1102
|
+
"start-date-desc": 2,
|
|
1103
|
+
"newly-added": 3
|
|
1104
|
+
};
|
|
1105
|
+
var GROUP_SORT_KEYS = [
|
|
1106
|
+
"most-events",
|
|
1107
|
+
"most-members",
|
|
1108
|
+
"newly-added"
|
|
1109
|
+
];
|
|
1110
|
+
var GROUP_SORT_MAP = {
|
|
1111
|
+
"most-events": 1,
|
|
1112
|
+
"most-members": 2,
|
|
1113
|
+
"newly-added": 3
|
|
1114
|
+
};
|
|
1115
|
+
var USER_SORT_KEYS = [
|
|
1116
|
+
"most-events",
|
|
1117
|
+
"most-followers",
|
|
1118
|
+
"newly-added"
|
|
1119
|
+
];
|
|
1120
|
+
var USER_SORT_MAP = {
|
|
1121
|
+
"most-events": 1,
|
|
1122
|
+
"most-followers": 2,
|
|
1123
|
+
"newly-added": 3
|
|
1124
|
+
};
|
|
1125
|
+
function formatAsCompactYmd(date) {
|
|
1126
|
+
const year = date.getFullYear();
|
|
1127
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1128
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1129
|
+
return `${year}${month}${day}`;
|
|
1130
|
+
}
|
|
1131
|
+
function formatAsHyphenatedYmd(date) {
|
|
1132
|
+
const year = date.getFullYear();
|
|
1133
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1134
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1135
|
+
return `${year}-${month}-${day}`;
|
|
1136
|
+
}
|
|
1137
|
+
function parseDateInput(input, options) {
|
|
1138
|
+
const normalized = input.trim();
|
|
1139
|
+
const hyphenFree = normalized.replace(/[-/.]/g, "");
|
|
1140
|
+
if (/^\d{8}$/.test(hyphenFree)) {
|
|
1141
|
+
const year = Number(hyphenFree.slice(0, 4));
|
|
1142
|
+
const month = Number(hyphenFree.slice(4, 6)) - 1;
|
|
1143
|
+
const day = Number(hyphenFree.slice(6, 8));
|
|
1144
|
+
const candidate = new Date(Date.UTC(year, month, day));
|
|
1145
|
+
if (!Number.isNaN(candidate.getTime())) {
|
|
1146
|
+
return options?.style === "hyphenated" ? formatAsHyphenatedYmd(candidate) : formatAsCompactYmd(candidate);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
`Invalid date format: ${input}. Expected YYYY-MM-DD or YYYYMMDD format.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
function toYmdArray(value) {
|
|
1154
|
+
if (!value) return void 0;
|
|
1155
|
+
const inputs = Array.isArray(value) ? value : [value];
|
|
1156
|
+
return inputs.map((item) => parseDateInput(item));
|
|
1157
|
+
}
|
|
1158
|
+
function parseHyphenatedDate(input) {
|
|
1159
|
+
const parsed = parseDateInput(input, { style: "hyphenated" });
|
|
1160
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(parsed)) return parsed;
|
|
1161
|
+
const digitsOnly = parsed.replace(/[^0-9]/g, "");
|
|
1162
|
+
if (digitsOnly.length === 8) {
|
|
1163
|
+
return `${digitsOnly.slice(0, 4)}-${digitsOnly.slice(4, 6)}-${digitsOnly.slice(6, 8)}`;
|
|
1164
|
+
}
|
|
1165
|
+
throw new Error(`Could not convert date input to YYYY-MM-DD: ${input}`);
|
|
1166
|
+
}
|
|
1167
|
+
function normalizeStringArray(value) {
|
|
1168
|
+
if (!value) return void 0;
|
|
1169
|
+
return Array.isArray(value) ? value : [value];
|
|
1170
|
+
}
|
|
1171
|
+
function normalizeKeywordOr(value) {
|
|
1172
|
+
if (!value) return void 0;
|
|
1173
|
+
const tokens = value.split(/[、,]+/).map((item) => item.trim()).filter(Boolean);
|
|
1174
|
+
if (tokens.length === 0) return void 0;
|
|
1175
|
+
return tokens.join(",");
|
|
1176
|
+
}
|
|
1177
|
+
function applyPagination(page, pageSize, options) {
|
|
1178
|
+
const includePagination = options?.includePagination ?? true;
|
|
1179
|
+
if (!includePagination) return {};
|
|
1180
|
+
const effectivePageSize = pageSize ?? DEFAULT_PAGE_SIZE;
|
|
1181
|
+
const pagination = {};
|
|
1182
|
+
if (page) {
|
|
1183
|
+
pagination.start = 1 + (page - 1) * effectivePageSize;
|
|
1184
|
+
pagination.count = effectivePageSize;
|
|
1185
|
+
} else if (pageSize) {
|
|
1186
|
+
pagination.count = effectivePageSize;
|
|
1187
|
+
}
|
|
1188
|
+
return pagination;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/tools/events.ts
|
|
1192
|
+
var EventSearchInputSchema = z2.object({
|
|
1193
|
+
anyQuery: z2.string().min(1).describe("Search keywords (comma-separated, matches ANY keyword)").optional(),
|
|
1194
|
+
on: z2.union([z2.string().min(1), z2.array(z2.string().min(1))]).describe("Specific date(s) in YYYY-MM-DD or YYYYMMDD format").optional(),
|
|
1195
|
+
from: z2.string().min(1).describe("Start of date range (inclusive). Format: YYYY-MM-DD").optional(),
|
|
1196
|
+
to: z2.string().min(1).describe("End of date range (inclusive). Format: YYYY-MM-DD").optional(),
|
|
1197
|
+
participantNickname: z2.string().min(1).describe("Filter by participant nickname").optional(),
|
|
1198
|
+
hostNickname: z2.string().min(1).describe("Filter by host/owner nickname").optional(),
|
|
1199
|
+
groupIds: z2.array(z2.number()).describe("Limit results to specific group IDs").optional(),
|
|
1200
|
+
prefectures: z2.union([z2.string().min(1), z2.array(z2.string().min(1))]).describe("Prefecture name(s) to filter by").optional(),
|
|
1201
|
+
page: z2.number().int().min(1).describe("1-based page number").optional(),
|
|
1202
|
+
pageSize: z2.number().int().min(1).max(100).describe("Events per page (default 20, max 100)").optional(),
|
|
1203
|
+
sort: z2.enum(EVENT_SORT_KEYS).describe("Sort order").optional(),
|
|
1204
|
+
includeDetails: z2.boolean().describe(
|
|
1205
|
+
"Include event description (up to 200 chars). Use when you need content details for recommendations."
|
|
1206
|
+
).default(false).optional()
|
|
1207
|
+
});
|
|
1208
|
+
var EventPresentationsInputSchema = z2.object({
|
|
1209
|
+
eventId: z2.union([z2.number().int().positive(), z2.string().min(1)]).describe("Connpass event ID").transform((value) => Number(value)).refine((value) => Number.isFinite(value), "eventId must be a number")
|
|
1210
|
+
});
|
|
1211
|
+
function buildEventSearchParams(input) {
|
|
1212
|
+
const pagination = applyPagination(input.page, input.pageSize);
|
|
1213
|
+
const resolved = resolvePrefectureInputs(input.prefectures);
|
|
1214
|
+
if ("response" in resolved) {
|
|
1215
|
+
return resolved;
|
|
1216
|
+
}
|
|
1217
|
+
return {
|
|
1218
|
+
keywordOr: normalizeKeywordOr(input.anyQuery),
|
|
1219
|
+
ymd: toYmdArray(input.on),
|
|
1220
|
+
ymdFrom: input.from ? parseHyphenatedDate(input.from) : void 0,
|
|
1221
|
+
ymdTo: input.to ? parseHyphenatedDate(input.to) : void 0,
|
|
1222
|
+
nickname: input.participantNickname,
|
|
1223
|
+
ownerNickname: input.hostNickname,
|
|
1224
|
+
groupId: input.groupIds,
|
|
1225
|
+
prefecture: resolved.prefectures ?? normalizeStringArray(input.prefectures),
|
|
1226
|
+
order: input.sort ? EVENT_SORT_MAP[input.sort] : void 0,
|
|
1227
|
+
...pagination
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
var LIST_FORMAT_OPTIONS = {
|
|
1231
|
+
descriptionLimit: 0,
|
|
1232
|
+
catchPhraseLimit: 100
|
|
1233
|
+
};
|
|
1234
|
+
function registerEventTools(deps) {
|
|
1235
|
+
const { server: server2, connpassClient, searchSessionStore } = deps;
|
|
1236
|
+
const searchEventsHandler = async (args) => {
|
|
1237
|
+
const params = EventSearchInputSchema.parse(args ?? {});
|
|
1238
|
+
const searchParams = buildEventSearchParams(params);
|
|
1239
|
+
if ("response" in searchParams) {
|
|
1240
|
+
return {
|
|
1241
|
+
...searchParams.response,
|
|
1242
|
+
isError: true
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const response = await connpassClient.searchEvents(searchParams);
|
|
1246
|
+
const formatOptions = params.includeDetails ? FORMAT_PRESETS.detailed : FORMAT_PRESETS.default;
|
|
1247
|
+
const formatted = formatEventsResponse(response, formatOptions);
|
|
1248
|
+
const browseFormatted = formatEventsResponse(response, LIST_FORMAT_OPTIONS);
|
|
1249
|
+
const searchSessionId = searchSessionStore.save(browseFormatted);
|
|
1250
|
+
return {
|
|
1251
|
+
content: [
|
|
1252
|
+
{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: summarizeEventsResponse(formatted, "events", {
|
|
1255
|
+
searchSessionId
|
|
1256
|
+
})
|
|
1257
|
+
}
|
|
1258
|
+
],
|
|
1259
|
+
structuredContent: {
|
|
1260
|
+
kind: "events",
|
|
1261
|
+
searchSessionId,
|
|
1262
|
+
data: formatted
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
};
|
|
1266
|
+
registerAppToolIfEnabled(
|
|
1267
|
+
server2,
|
|
1268
|
+
"search_events",
|
|
1269
|
+
{
|
|
1270
|
+
title: "Search Events",
|
|
1271
|
+
description: "Search Connpass events and return results as text for reasoning and recommendations. Use this whenever the user asks about events.",
|
|
1272
|
+
inputSchema: EventSearchInputSchema
|
|
1273
|
+
},
|
|
1274
|
+
searchEventsHandler
|
|
1275
|
+
);
|
|
1276
|
+
registerAppToolIfEnabled(
|
|
1277
|
+
server2,
|
|
1278
|
+
"browse_events",
|
|
1279
|
+
{
|
|
1280
|
+
title: "Browse Events",
|
|
1281
|
+
description: "Display previously searched events in the interactive event browser UI. Use this proactively when the user wants to browse event options, scan many candidates, compare events, or inspect results visually. The UI lets the user refine and re-run searches directly, so prefer this for event exploration. Call this ONCE with the searchSessionId returned by search_events. Do not use this to search again.",
|
|
1282
|
+
inputSchema: z2.object({
|
|
1283
|
+
searchSessionId: z2.uuid().describe("Session ID returned by search_events")
|
|
1284
|
+
}),
|
|
1285
|
+
_meta: {
|
|
1286
|
+
ui: { resourceUri: connpassResourceUri }
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
async (args) => {
|
|
1290
|
+
const { searchSessionId } = z2.object({
|
|
1291
|
+
searchSessionId: z2.uuid()
|
|
1292
|
+
}).parse(args ?? {});
|
|
1293
|
+
const formatted = searchSessionStore.get(searchSessionId);
|
|
1294
|
+
if (!formatted) {
|
|
1295
|
+
return {
|
|
1296
|
+
content: [
|
|
1297
|
+
{
|
|
1298
|
+
type: "text",
|
|
1299
|
+
text: "Search session not found or expired. Run search_events again, then call browse_events once with the returned searchSessionId."
|
|
1300
|
+
}
|
|
1301
|
+
],
|
|
1302
|
+
isError: true
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
content: [
|
|
1307
|
+
{
|
|
1308
|
+
type: "text",
|
|
1309
|
+
text: `Displayed ${formatted.events.length} of ${formatted.available} events in the browser UI.`
|
|
1310
|
+
}
|
|
1311
|
+
],
|
|
1312
|
+
structuredContent: {
|
|
1313
|
+
kind: "events",
|
|
1314
|
+
searchSessionId,
|
|
1315
|
+
data: formatted
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
registerAppToolIfEnabled(
|
|
1321
|
+
server2,
|
|
1322
|
+
"get_event_presentations",
|
|
1323
|
+
{
|
|
1324
|
+
title: "Get Event Presentations",
|
|
1325
|
+
description: "Look up presentation details for a specific event",
|
|
1326
|
+
inputSchema: EventPresentationsInputSchema
|
|
1327
|
+
},
|
|
1328
|
+
async (args) => {
|
|
1329
|
+
const { eventId } = EventPresentationsInputSchema.parse(args ?? {});
|
|
1330
|
+
const response = await connpassClient.getEventPresentations(eventId);
|
|
1331
|
+
const formatted = formatPresentationsResponse(response);
|
|
1332
|
+
return {
|
|
1333
|
+
content: [{ type: "text", text: JSON.stringify(formatted) }]
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
registerAppToolIfEnabled(
|
|
1338
|
+
server2,
|
|
1339
|
+
"get_event_detail",
|
|
1340
|
+
{
|
|
1341
|
+
title: "Get Event Detail",
|
|
1342
|
+
description: "Get full details of a specific event by ID, including complete description and presentations",
|
|
1343
|
+
inputSchema: EventPresentationsInputSchema
|
|
1344
|
+
},
|
|
1345
|
+
async (args) => {
|
|
1346
|
+
const { eventId } = EventPresentationsInputSchema.parse(args ?? {});
|
|
1347
|
+
const [eventsResponse, presentationsResponse] = await Promise.all([
|
|
1348
|
+
connpassClient.searchEvents({ eventId: [eventId], count: 1 }),
|
|
1349
|
+
connpassClient.getEventPresentations(eventId).catch(() => void 0)
|
|
1350
|
+
]);
|
|
1351
|
+
const event = eventsResponse.events[0];
|
|
1352
|
+
if (!event) {
|
|
1353
|
+
throw new Error(`Event with ID ${eventId} not found.`);
|
|
1354
|
+
}
|
|
1355
|
+
const formatted = formatEvent(event, FORMAT_PRESETS.full);
|
|
1356
|
+
const presentations = presentationsResponse ? formatPresentationsResponse(presentationsResponse) : void 0;
|
|
1357
|
+
return {
|
|
1358
|
+
content: [
|
|
1359
|
+
{
|
|
1360
|
+
type: "text",
|
|
1361
|
+
text: JSON.stringify({ event: formatted, presentations })
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/tools/groups.ts
|
|
1370
|
+
import { z as z3 } from "zod";
|
|
1371
|
+
var GroupSearchInputSchema = z3.object({
|
|
1372
|
+
query: z3.string().min(1).describe("Keywords matching group title or description").optional(),
|
|
1373
|
+
groupIds: z3.array(z3.number()).describe("Limit to specific group IDs").optional(),
|
|
1374
|
+
country: z3.string().min(1).describe("ISO country code, e.g. 'JP'").optional(),
|
|
1375
|
+
prefecture: z3.string().min(1).describe("Prefecture name to filter by").optional(),
|
|
1376
|
+
page: z3.number().int().min(1).describe("1-based page number").optional(),
|
|
1377
|
+
pageSize: z3.number().int().min(1).max(100).describe("Groups per page (default 20)").optional(),
|
|
1378
|
+
sort: z3.enum(GROUP_SORT_KEYS).describe("Ranking by activity, members, or recency").optional()
|
|
1379
|
+
});
|
|
1380
|
+
function buildGroupSearchParams(input) {
|
|
1381
|
+
const pagination = applyPagination(input.page, input.pageSize);
|
|
1382
|
+
const resolved = resolvePrefectureInputs(input.prefecture);
|
|
1383
|
+
if ("response" in resolved) {
|
|
1384
|
+
return resolved;
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
keyword: input.query,
|
|
1388
|
+
groupId: input.groupIds,
|
|
1389
|
+
countryCode: input.country,
|
|
1390
|
+
prefecture: resolved.prefectures?.[0] ?? input.prefecture,
|
|
1391
|
+
order: input.sort ? GROUP_SORT_MAP[input.sort] : void 0,
|
|
1392
|
+
...pagination
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function registerGroupTools(deps) {
|
|
1396
|
+
const { server: server2, connpassClient } = deps;
|
|
1397
|
+
registerAppToolIfEnabled(
|
|
1398
|
+
server2,
|
|
1399
|
+
"search_groups",
|
|
1400
|
+
{
|
|
1401
|
+
title: "Search Groups",
|
|
1402
|
+
description: "Find Connpass groups with simple filters",
|
|
1403
|
+
inputSchema: GroupSearchInputSchema
|
|
1404
|
+
},
|
|
1405
|
+
async (args) => {
|
|
1406
|
+
const params = GroupSearchInputSchema.parse(args ?? {});
|
|
1407
|
+
const searchParams = buildGroupSearchParams(params);
|
|
1408
|
+
if ("response" in searchParams) {
|
|
1409
|
+
return {
|
|
1410
|
+
...searchParams.response,
|
|
1411
|
+
isError: true
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
const response = await connpassClient.searchGroups(searchParams);
|
|
1415
|
+
return {
|
|
1416
|
+
content: [
|
|
1417
|
+
{ type: "text", text: summarizeGroupsResponse(response) }
|
|
1418
|
+
]
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/tools/schedule.ts
|
|
1425
|
+
import { z as z4 } from "zod";
|
|
1426
|
+
var ScheduleInputSchema = z4.object({
|
|
1427
|
+
userId: z4.number().int().positive().describe("Connpass user ID").optional(),
|
|
1428
|
+
nickname: z4.string().min(1).describe("Connpass user nickname").optional(),
|
|
1429
|
+
fromDate: z4.string().min(1).describe("Start date (YYYY-MM-DD). Defaults to today.").optional(),
|
|
1430
|
+
toDate: z4.string().min(1).describe("End date (YYYY-MM-DD). Defaults to +7 days.").optional(),
|
|
1431
|
+
maxEvents: z4.number().int().min(1).max(100).describe("Maximum events to check (default 30)").optional(),
|
|
1432
|
+
includeDetails: z4.boolean().describe("Include event description (up to 200 chars)").default(false).optional()
|
|
1433
|
+
});
|
|
1434
|
+
function startOfDay(date) {
|
|
1435
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1436
|
+
}
|
|
1437
|
+
function formatDateLabel(date) {
|
|
1438
|
+
const year = date.getFullYear();
|
|
1439
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1440
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1441
|
+
return `${year}-${month}-${day}`;
|
|
1442
|
+
}
|
|
1443
|
+
function registerScheduleTools(deps) {
|
|
1444
|
+
const { server: server2, connpassClient, config: config2 } = deps;
|
|
1445
|
+
registerAppToolIfEnabled(
|
|
1446
|
+
server2,
|
|
1447
|
+
"search_schedule",
|
|
1448
|
+
{
|
|
1449
|
+
title: "Search Schedule",
|
|
1450
|
+
description: "Search user's schedule - events within a date range",
|
|
1451
|
+
inputSchema: ScheduleInputSchema,
|
|
1452
|
+
_meta: {
|
|
1453
|
+
ui: { resourceUri: connpassResourceUri }
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
async (args) => {
|
|
1457
|
+
const parsed = ScheduleInputSchema.parse(args ?? {});
|
|
1458
|
+
let resolvedUserId = parsed.userId ?? config2.defaultUserId;
|
|
1459
|
+
let userNickname;
|
|
1460
|
+
if (parsed.nickname) {
|
|
1461
|
+
const userSearchResponse = await connpassClient.searchUsers({
|
|
1462
|
+
nickname: parsed.nickname
|
|
1463
|
+
});
|
|
1464
|
+
if (userSearchResponse.users.length === 0) {
|
|
1465
|
+
throw new Error(`User with nickname "${parsed.nickname}" not found.`);
|
|
1466
|
+
}
|
|
1467
|
+
resolvedUserId = userSearchResponse.users[0].id;
|
|
1468
|
+
userNickname = userSearchResponse.users[0].nickname;
|
|
1469
|
+
}
|
|
1470
|
+
if (!resolvedUserId) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
"User ID or nickname is required. Pass userId, nickname, or set CONNPASS_DEFAULT_USER_ID."
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const maxEventsToFetch = parsed.maxEvents ?? 30;
|
|
1476
|
+
const today = startOfDay(/* @__PURE__ */ new Date());
|
|
1477
|
+
const rangeStart = parsed.fromDate ? startOfDay(new Date(parseHyphenatedDate(parsed.fromDate))) : today;
|
|
1478
|
+
const rangeEnd = parsed.toDate ? startOfDay(new Date(parseHyphenatedDate(parsed.toDate))) : (() => {
|
|
1479
|
+
const defaultEnd = new Date(rangeStart);
|
|
1480
|
+
defaultEnd.setDate(defaultEnd.getDate() + 7);
|
|
1481
|
+
return defaultEnd;
|
|
1482
|
+
})();
|
|
1483
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
1484
|
+
if (!userNickname) {
|
|
1485
|
+
const userResponse = await connpassClient.searchUsers({
|
|
1486
|
+
userId: [resolvedUserId]
|
|
1487
|
+
});
|
|
1488
|
+
if (userResponse.users.length === 0) {
|
|
1489
|
+
throw new Error(`User with ID ${resolvedUserId} not found.`);
|
|
1490
|
+
}
|
|
1491
|
+
userNickname = userResponse.users.find(
|
|
1492
|
+
(u) => u.id === resolvedUserId
|
|
1493
|
+
)?.nickname;
|
|
1494
|
+
if (!userNickname)
|
|
1495
|
+
throw new Error(`User with ID ${resolvedUserId} not found.`);
|
|
1496
|
+
}
|
|
1497
|
+
const searchResponse = await connpassClient.searchEvents({
|
|
1498
|
+
nickname: userNickname,
|
|
1499
|
+
ymdFrom: formatDateLabel(rangeStart),
|
|
1500
|
+
ymdTo: formatDateLabel(rangeEnd),
|
|
1501
|
+
order: EVENT_SORT_MAP["start-date-asc"],
|
|
1502
|
+
count: maxEventsToFetch
|
|
1503
|
+
});
|
|
1504
|
+
const eventsByDate = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const event of searchResponse.events) {
|
|
1506
|
+
const eventDate = formatDateLabel(
|
|
1507
|
+
startOfDay(new Date(event.startedAt))
|
|
1508
|
+
);
|
|
1509
|
+
if (!eventsByDate.has(eventDate)) {
|
|
1510
|
+
eventsByDate.set(eventDate, []);
|
|
1511
|
+
}
|
|
1512
|
+
eventsByDate.get(eventDate).push(event);
|
|
1513
|
+
}
|
|
1514
|
+
const sortedDates = Array.from(eventsByDate.keys()).sort();
|
|
1515
|
+
const sections = sortedDates.map((date) => ({
|
|
1516
|
+
date,
|
|
1517
|
+
events: formatEventList(
|
|
1518
|
+
eventsByDate.get(date),
|
|
1519
|
+
parsed.includeDetails ? FORMAT_PRESETS.detailed : FORMAT_PRESETS.default
|
|
1520
|
+
)
|
|
1521
|
+
}));
|
|
1522
|
+
const result = {
|
|
1523
|
+
userId: resolvedUserId,
|
|
1524
|
+
sections,
|
|
1525
|
+
metadata: {
|
|
1526
|
+
fromDate: formatDateLabel(rangeStart),
|
|
1527
|
+
toDate: formatDateLabel(rangeEnd),
|
|
1528
|
+
inspected: searchResponse.eventsReturned,
|
|
1529
|
+
limit: maxEventsToFetch
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
return {
|
|
1533
|
+
content: [
|
|
1534
|
+
{
|
|
1535
|
+
type: "text",
|
|
1536
|
+
text: summarizeEventsResponse(
|
|
1537
|
+
{
|
|
1538
|
+
returned: sections.reduce(
|
|
1539
|
+
(count, section) => count + section.events.length,
|
|
1540
|
+
0
|
|
1541
|
+
),
|
|
1542
|
+
available: sections.reduce(
|
|
1543
|
+
(count, section) => count + section.events.length,
|
|
1544
|
+
0
|
|
1545
|
+
),
|
|
1546
|
+
start: 1,
|
|
1547
|
+
events: sections.flatMap((section) => section.events)
|
|
1548
|
+
},
|
|
1549
|
+
"schedule"
|
|
1550
|
+
)
|
|
1551
|
+
}
|
|
1552
|
+
],
|
|
1553
|
+
structuredContent: {
|
|
1554
|
+
kind: "schedule",
|
|
1555
|
+
data: result
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/tools/ui-tools/detail.ts
|
|
1563
|
+
import { z as z5 } from "zod";
|
|
1564
|
+
var UIEventDetailInputSchema = z5.object({
|
|
1565
|
+
eventId: z5.number().int().positive()
|
|
1566
|
+
});
|
|
1567
|
+
function registerUIEventDetailTool(deps) {
|
|
1568
|
+
const { server: server2, connpassClient } = deps;
|
|
1569
|
+
registerAppToolIfEnabled(
|
|
1570
|
+
server2,
|
|
1571
|
+
"_get_event_detail",
|
|
1572
|
+
{
|
|
1573
|
+
title: "Get Event Detail (UI)",
|
|
1574
|
+
description: "Internal: fetch full event details for the detail view",
|
|
1575
|
+
inputSchema: UIEventDetailInputSchema,
|
|
1576
|
+
_meta: {
|
|
1577
|
+
ui: {
|
|
1578
|
+
resourceUri: connpassResourceUri,
|
|
1579
|
+
visibility: ["app"]
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
async (args) => {
|
|
1584
|
+
const { eventId } = UIEventDetailInputSchema.parse(args ?? {});
|
|
1585
|
+
const [eventsResponse, presentationsResponse] = await Promise.all([
|
|
1586
|
+
connpassClient.searchEvents({
|
|
1587
|
+
eventId: [eventId],
|
|
1588
|
+
count: 1
|
|
1589
|
+
}),
|
|
1590
|
+
connpassClient.getEventPresentations(eventId).catch(() => void 0)
|
|
1591
|
+
]);
|
|
1592
|
+
const event = eventsResponse.events[0];
|
|
1593
|
+
if (!event) {
|
|
1594
|
+
throw new Error(`Event with ID ${eventId} not found.`);
|
|
1595
|
+
}
|
|
1596
|
+
const formatted = formatEvent(event);
|
|
1597
|
+
const presentations = presentationsResponse ? formatPresentationsResponse(presentationsResponse) : void 0;
|
|
1598
|
+
const detailEvent = presentations?.presentations?.length ? { ...formatted, presentations: presentations.presentations } : formatted;
|
|
1599
|
+
return {
|
|
1600
|
+
content: [{ type: "text", text: JSON.stringify(detailEvent) }],
|
|
1601
|
+
structuredContent: {
|
|
1602
|
+
kind: "event-detail",
|
|
1603
|
+
data: {
|
|
1604
|
+
event: detailEvent,
|
|
1605
|
+
presentations
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/tools/ui-tools/schedule.ts
|
|
1614
|
+
import { z as z6 } from "zod";
|
|
1615
|
+
var UIScheduleInputSchema = z6.object({
|
|
1616
|
+
userId: z6.number().int().positive().optional(),
|
|
1617
|
+
nickname: z6.string().min(1).optional(),
|
|
1618
|
+
fromDate: z6.string().min(1).optional(),
|
|
1619
|
+
toDate: z6.string().min(1).optional(),
|
|
1620
|
+
maxEvents: z6.number().int().min(1).max(100).optional()
|
|
1621
|
+
});
|
|
1622
|
+
function startOfDay2(date) {
|
|
1623
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1624
|
+
}
|
|
1625
|
+
function formatDateLabel2(date) {
|
|
1626
|
+
const year = date.getFullYear();
|
|
1627
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1628
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1629
|
+
return `${year}-${month}-${day}`;
|
|
1630
|
+
}
|
|
1631
|
+
var SCHEDULE_FORMAT_OPTIONS = {
|
|
1632
|
+
descriptionLimit: 0,
|
|
1633
|
+
catchPhraseLimit: 100
|
|
1634
|
+
};
|
|
1635
|
+
var userNicknameCache = /* @__PURE__ */ new Map();
|
|
1636
|
+
function registerUIScheduleTool(deps) {
|
|
1637
|
+
const { server: server2, connpassClient, config: config2 } = deps;
|
|
1638
|
+
registerAppToolIfEnabled(
|
|
1639
|
+
server2,
|
|
1640
|
+
"_search_schedule",
|
|
1641
|
+
{
|
|
1642
|
+
title: "Search Schedule (UI)",
|
|
1643
|
+
description: "Internal: re-search schedule from UI",
|
|
1644
|
+
inputSchema: UIScheduleInputSchema,
|
|
1645
|
+
_meta: {
|
|
1646
|
+
ui: {
|
|
1647
|
+
resourceUri: connpassResourceUri,
|
|
1648
|
+
visibility: ["app"]
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
async (args) => {
|
|
1653
|
+
const parsed = UIScheduleInputSchema.parse(args ?? {});
|
|
1654
|
+
let resolvedUserId = parsed.userId ?? config2.defaultUserId;
|
|
1655
|
+
let userNickname;
|
|
1656
|
+
if (parsed.nickname) {
|
|
1657
|
+
const userSearchResponse = await connpassClient.searchUsers({
|
|
1658
|
+
nickname: parsed.nickname
|
|
1659
|
+
});
|
|
1660
|
+
if (userSearchResponse.users.length === 0) {
|
|
1661
|
+
throw new Error(`User with nickname "${parsed.nickname}" not found.`);
|
|
1662
|
+
}
|
|
1663
|
+
resolvedUserId = userSearchResponse.users[0].id;
|
|
1664
|
+
userNickname = userSearchResponse.users[0].nickname;
|
|
1665
|
+
userNicknameCache.set(resolvedUserId, userNickname);
|
|
1666
|
+
}
|
|
1667
|
+
if (!resolvedUserId) {
|
|
1668
|
+
throw new Error("User ID or nickname is required.");
|
|
1669
|
+
}
|
|
1670
|
+
const maxEventsToFetch = parsed.maxEvents ?? 30;
|
|
1671
|
+
const today = startOfDay2(/* @__PURE__ */ new Date());
|
|
1672
|
+
const rangeStart = parsed.fromDate ? startOfDay2(new Date(parseHyphenatedDate(parsed.fromDate))) : today;
|
|
1673
|
+
const rangeEnd = parsed.toDate ? startOfDay2(new Date(parseHyphenatedDate(parsed.toDate))) : (() => {
|
|
1674
|
+
const defaultEnd = new Date(rangeStart);
|
|
1675
|
+
defaultEnd.setDate(defaultEnd.getDate() + 7);
|
|
1676
|
+
return defaultEnd;
|
|
1677
|
+
})();
|
|
1678
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
1679
|
+
if (!userNickname) {
|
|
1680
|
+
userNickname = userNicknameCache.get(resolvedUserId);
|
|
1681
|
+
if (!userNickname) {
|
|
1682
|
+
const userResponse = await connpassClient.searchUsers({
|
|
1683
|
+
userId: [resolvedUserId]
|
|
1684
|
+
});
|
|
1685
|
+
if (userResponse.users.length === 0)
|
|
1686
|
+
throw new Error(`User with ID ${resolvedUserId} not found.`);
|
|
1687
|
+
userNickname = userResponse.users.find(
|
|
1688
|
+
(u) => u.id === resolvedUserId
|
|
1689
|
+
)?.nickname;
|
|
1690
|
+
if (!userNickname)
|
|
1691
|
+
throw new Error(`User with ID ${resolvedUserId} not found.`);
|
|
1692
|
+
userNicknameCache.set(resolvedUserId, userNickname);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const searchResponse = await connpassClient.searchEvents({
|
|
1696
|
+
nickname: userNickname,
|
|
1697
|
+
ymdFrom: formatDateLabel2(rangeStart),
|
|
1698
|
+
ymdTo: formatDateLabel2(rangeEnd),
|
|
1699
|
+
order: EVENT_SORT_MAP["start-date-asc"],
|
|
1700
|
+
count: maxEventsToFetch
|
|
1701
|
+
});
|
|
1702
|
+
const eventsByDate = /* @__PURE__ */ new Map();
|
|
1703
|
+
for (const event of searchResponse.events) {
|
|
1704
|
+
const eventDate = formatDateLabel2(
|
|
1705
|
+
startOfDay2(new Date(event.startedAt))
|
|
1706
|
+
);
|
|
1707
|
+
if (!eventsByDate.has(eventDate)) eventsByDate.set(eventDate, []);
|
|
1708
|
+
eventsByDate.get(eventDate).push(event);
|
|
1709
|
+
}
|
|
1710
|
+
const sortedDates = Array.from(eventsByDate.keys()).sort();
|
|
1711
|
+
const sections = sortedDates.map((date) => ({
|
|
1712
|
+
date,
|
|
1713
|
+
events: formatEventList(
|
|
1714
|
+
eventsByDate.get(date),
|
|
1715
|
+
SCHEDULE_FORMAT_OPTIONS
|
|
1716
|
+
)
|
|
1717
|
+
}));
|
|
1718
|
+
const result = {
|
|
1719
|
+
userId: resolvedUserId,
|
|
1720
|
+
sections,
|
|
1721
|
+
metadata: {
|
|
1722
|
+
fromDate: formatDateLabel2(rangeStart),
|
|
1723
|
+
toDate: formatDateLabel2(rangeEnd),
|
|
1724
|
+
inspected: searchResponse.eventsReturned,
|
|
1725
|
+
limit: maxEventsToFetch
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
1730
|
+
structuredContent: {
|
|
1731
|
+
kind: "schedule",
|
|
1732
|
+
data: result
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/tools/ui-tools/search.ts
|
|
1740
|
+
import { z as z7 } from "zod";
|
|
1741
|
+
var UI_LIST_FORMAT_OPTIONS = {
|
|
1742
|
+
descriptionLimit: 0,
|
|
1743
|
+
catchPhraseLimit: 100
|
|
1744
|
+
};
|
|
1745
|
+
var UIEventSearchInputSchema = z7.object({
|
|
1746
|
+
query: z7.string().min(1).optional(),
|
|
1747
|
+
anyQuery: z7.string().min(1).optional(),
|
|
1748
|
+
on: z7.union([z7.string().min(1), z7.array(z7.string().min(1))]).optional(),
|
|
1749
|
+
from: z7.string().min(1).optional(),
|
|
1750
|
+
to: z7.string().min(1).optional(),
|
|
1751
|
+
participantNickname: z7.string().min(1).optional(),
|
|
1752
|
+
hostNickname: z7.string().min(1).optional(),
|
|
1753
|
+
groupIds: z7.array(z7.number()).optional(),
|
|
1754
|
+
prefectures: z7.union([z7.string().min(1), z7.array(z7.string().min(1))]).optional(),
|
|
1755
|
+
companyQuery: z7.string().min(1).optional(),
|
|
1756
|
+
minAccepted: z7.number().int().min(0).optional(),
|
|
1757
|
+
maxAccepted: z7.number().int().min(0).optional(),
|
|
1758
|
+
minCapacity: z7.number().int().min(0).optional(),
|
|
1759
|
+
maxCapacity: z7.number().int().min(0).optional(),
|
|
1760
|
+
page: z7.number().int().min(1).optional(),
|
|
1761
|
+
pageSize: z7.number().int().min(1).max(100).optional(),
|
|
1762
|
+
sort: z7.enum(["start-date-asc", "participant-count-desc", "title-asc"]).optional()
|
|
1763
|
+
});
|
|
1764
|
+
function includesNormalized(targets, query) {
|
|
1765
|
+
if (!query) return true;
|
|
1766
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1767
|
+
if (!normalizedQuery) return true;
|
|
1768
|
+
return targets.some(
|
|
1769
|
+
(value) => value?.toLowerCase().includes(normalizedQuery)
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
function applyLocalFilters(events, params) {
|
|
1773
|
+
return events.filter((event) => {
|
|
1774
|
+
if (!includesNormalized(
|
|
1775
|
+
[event.ownerDisplayName, event.groupTitle],
|
|
1776
|
+
params.companyQuery
|
|
1777
|
+
)) {
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
if (typeof params.minAccepted === "number" && event.participantCount < params.minAccepted) {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
if (typeof params.maxAccepted === "number" && event.participantCount > params.maxAccepted) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
if (typeof params.minCapacity === "number") {
|
|
1787
|
+
const limit = typeof event.limit === "number" ? event.limit : 0;
|
|
1788
|
+
if (limit < params.minCapacity) return false;
|
|
1789
|
+
}
|
|
1790
|
+
if (typeof params.maxCapacity === "number") {
|
|
1791
|
+
const limit = typeof event.limit === "number" ? event.limit : 0;
|
|
1792
|
+
if (limit > params.maxCapacity) return false;
|
|
1793
|
+
}
|
|
1794
|
+
return true;
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
function sortEvents(events, sort) {
|
|
1798
|
+
const copied = [...events];
|
|
1799
|
+
copied.sort((a, b) => {
|
|
1800
|
+
if (sort === "participant-count-desc") {
|
|
1801
|
+
return b.participantCount - a.participantCount || a.startedAt.localeCompare(b.startedAt);
|
|
1802
|
+
}
|
|
1803
|
+
if (sort === "title-asc") {
|
|
1804
|
+
return a.title.localeCompare(b.title, "ja") || a.startedAt.localeCompare(b.startedAt);
|
|
1805
|
+
}
|
|
1806
|
+
return a.startedAt.localeCompare(b.startedAt);
|
|
1807
|
+
});
|
|
1808
|
+
return copied;
|
|
1809
|
+
}
|
|
1810
|
+
function registerUISearchTool(deps) {
|
|
1811
|
+
const { server: server2, connpassClient } = deps;
|
|
1812
|
+
registerAppToolIfEnabled(
|
|
1813
|
+
server2,
|
|
1814
|
+
"_search_events",
|
|
1815
|
+
{
|
|
1816
|
+
title: "Search Events (UI)",
|
|
1817
|
+
description: "Internal: re-search events from UI",
|
|
1818
|
+
inputSchema: UIEventSearchInputSchema,
|
|
1819
|
+
_meta: {
|
|
1820
|
+
ui: {
|
|
1821
|
+
resourceUri: connpassResourceUri,
|
|
1822
|
+
visibility: ["app"]
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
async (args) => {
|
|
1827
|
+
const params = UIEventSearchInputSchema.parse(args ?? {});
|
|
1828
|
+
const useLocalFiltering = Boolean(params.companyQuery) || typeof params.minAccepted === "number" || typeof params.maxAccepted === "number" || typeof params.minCapacity === "number" || typeof params.maxCapacity === "number" || params.sort === "participant-count-desc" || params.sort === "title-asc";
|
|
1829
|
+
const pagination = useLocalFiltering ? {} : applyPagination(params.page, params.pageSize);
|
|
1830
|
+
const resolved = resolvePrefectureInputs(params.prefectures);
|
|
1831
|
+
if ("response" in resolved) {
|
|
1832
|
+
return {
|
|
1833
|
+
...resolved.response,
|
|
1834
|
+
isError: true
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
const searchParams = {
|
|
1838
|
+
keyword: params.query,
|
|
1839
|
+
keywordOr: normalizeKeywordOr(params.anyQuery),
|
|
1840
|
+
ymd: toYmdArray(params.on),
|
|
1841
|
+
ymdFrom: params.from ? parseHyphenatedDate(params.from) : void 0,
|
|
1842
|
+
ymdTo: params.to ? parseHyphenatedDate(params.to) : void 0,
|
|
1843
|
+
nickname: params.participantNickname,
|
|
1844
|
+
ownerNickname: params.hostNickname,
|
|
1845
|
+
groupId: params.groupIds,
|
|
1846
|
+
prefecture: resolved.prefectures ?? normalizeStringArray(params.prefectures),
|
|
1847
|
+
order: EVENT_SORT_MAP["start-date-asc"],
|
|
1848
|
+
...pagination
|
|
1849
|
+
};
|
|
1850
|
+
if (!useLocalFiltering) {
|
|
1851
|
+
const response2 = await connpassClient.searchEvents(searchParams);
|
|
1852
|
+
const formatted2 = formatEventsResponse(
|
|
1853
|
+
response2,
|
|
1854
|
+
UI_LIST_FORMAT_OPTIONS
|
|
1855
|
+
);
|
|
1856
|
+
return {
|
|
1857
|
+
content: [{ type: "text", text: JSON.stringify(formatted2) }],
|
|
1858
|
+
structuredContent: {
|
|
1859
|
+
kind: "events",
|
|
1860
|
+
data: formatted2
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
const response = await connpassClient.getAllEvents(searchParams);
|
|
1865
|
+
const filtered = sortEvents(
|
|
1866
|
+
applyLocalFilters(response.events, params),
|
|
1867
|
+
params.sort
|
|
1868
|
+
);
|
|
1869
|
+
const pageSize = params.pageSize ?? 20;
|
|
1870
|
+
const page = params.page ?? 1;
|
|
1871
|
+
const sliceStart = (page - 1) * pageSize;
|
|
1872
|
+
const paged = filtered.slice(sliceStart, sliceStart + pageSize);
|
|
1873
|
+
const formatted = {
|
|
1874
|
+
returned: paged.length,
|
|
1875
|
+
available: filtered.length,
|
|
1876
|
+
start: filtered.length === 0 ? 0 : sliceStart + 1,
|
|
1877
|
+
events: paged.map(
|
|
1878
|
+
(event) => formatEvent(event, UI_LIST_FORMAT_OPTIONS)
|
|
1879
|
+
)
|
|
1880
|
+
};
|
|
1881
|
+
return {
|
|
1882
|
+
content: [{ type: "text", text: JSON.stringify(formatted) }],
|
|
1883
|
+
structuredContent: {
|
|
1884
|
+
kind: "events",
|
|
1885
|
+
data: formatted
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// src/tools/ui-tools/index.ts
|
|
1893
|
+
function registerUITools(deps) {
|
|
1894
|
+
registerUIEventDetailTool(deps);
|
|
1895
|
+
registerUISearchTool(deps);
|
|
1896
|
+
registerUIScheduleTool(deps);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/tools/users.ts
|
|
1900
|
+
import { z as z8 } from "zod";
|
|
1901
|
+
var UserSearchInputSchema = z8.object({
|
|
1902
|
+
nickname: z8.string().min(1).describe("Match users by nickname (substring search)").optional(),
|
|
1903
|
+
userIds: z8.array(z8.number()).describe("Limit to specific user IDs").optional(),
|
|
1904
|
+
page: z8.number().int().min(1).describe("1-based page number").optional(),
|
|
1905
|
+
pageSize: z8.number().int().min(1).max(100).describe("Users per page (default 20)").optional(),
|
|
1906
|
+
sort: z8.enum(USER_SORT_KEYS).describe("Ranking by activity, followers, or recency").optional()
|
|
1907
|
+
});
|
|
1908
|
+
var UserGroupsInputSchema = z8.object({
|
|
1909
|
+
userId: z8.number().int().positive().describe("Connpass user ID"),
|
|
1910
|
+
limit: z8.number().int().min(1).max(100).describe("How many to return (default 20)").optional(),
|
|
1911
|
+
page: z8.number().int().min(1).describe("1-based page number").optional()
|
|
1912
|
+
});
|
|
1913
|
+
var UserRelationshipInputSchema = z8.object({
|
|
1914
|
+
userId: z8.number().int().positive().describe("Connpass user ID"),
|
|
1915
|
+
limit: z8.number().int().min(1).max(100).describe("How many to return (default 20)").optional(),
|
|
1916
|
+
page: z8.number().int().min(1).describe("1-based page number").optional(),
|
|
1917
|
+
sort: z8.enum(EVENT_SORT_KEYS).describe("Sort events by schedule or recency").optional(),
|
|
1918
|
+
includeDetails: z8.boolean().describe(
|
|
1919
|
+
"Include event description (up to 200 chars). Use when you need content details for recommendations."
|
|
1920
|
+
).default(false).optional()
|
|
1921
|
+
});
|
|
1922
|
+
function buildUserRelationshipParams(input) {
|
|
1923
|
+
const pagination = applyPagination(input.page, input.limit, {
|
|
1924
|
+
includePagination: true
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
pagination,
|
|
1928
|
+
order: input.sort ? EVENT_SORT_MAP[input.sort] : void 0
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function registerUserTools(deps) {
|
|
1932
|
+
const { server: server2, connpassClient } = deps;
|
|
1933
|
+
registerAppToolIfEnabled(
|
|
1934
|
+
server2,
|
|
1935
|
+
"search_users",
|
|
1936
|
+
{
|
|
1937
|
+
title: "Search Users",
|
|
1938
|
+
description: "Discover Connpass users",
|
|
1939
|
+
inputSchema: UserSearchInputSchema
|
|
1940
|
+
},
|
|
1941
|
+
async (args) => {
|
|
1942
|
+
const params = UserSearchInputSchema.parse(args ?? {});
|
|
1943
|
+
const pagination = applyPagination(params.page, params.pageSize);
|
|
1944
|
+
const response = await connpassClient.searchUsers({
|
|
1945
|
+
nickname: params.nickname,
|
|
1946
|
+
userId: params.userIds,
|
|
1947
|
+
order: params.sort ? USER_SORT_MAP[params.sort] : void 0,
|
|
1948
|
+
...pagination
|
|
1949
|
+
});
|
|
1950
|
+
return {
|
|
1951
|
+
content: [
|
|
1952
|
+
{ type: "text", text: summarizeUsersResponse(response) }
|
|
1953
|
+
]
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
);
|
|
1957
|
+
registerAppToolIfEnabled(
|
|
1958
|
+
server2,
|
|
1959
|
+
"get_user_groups",
|
|
1960
|
+
{
|
|
1961
|
+
title: "Get User Groups",
|
|
1962
|
+
description: "List the groups a user belongs to",
|
|
1963
|
+
inputSchema: UserGroupsInputSchema
|
|
1964
|
+
},
|
|
1965
|
+
async (args) => {
|
|
1966
|
+
const { userId, limit, page } = UserGroupsInputSchema.parse(args ?? {});
|
|
1967
|
+
const pagination = applyPagination(page, limit);
|
|
1968
|
+
const response = await connpassClient.getUserGroups(userId, pagination);
|
|
1969
|
+
return {
|
|
1970
|
+
content: [
|
|
1971
|
+
{ type: "text", text: summarizeGroupsResponse(response) }
|
|
1972
|
+
]
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
);
|
|
1976
|
+
registerAppToolIfEnabled(
|
|
1977
|
+
server2,
|
|
1978
|
+
"get_user_attended_events",
|
|
1979
|
+
{
|
|
1980
|
+
title: "Get User Attended Events",
|
|
1981
|
+
description: "List events that a user has attended",
|
|
1982
|
+
inputSchema: UserRelationshipInputSchema
|
|
1983
|
+
},
|
|
1984
|
+
async (args) => {
|
|
1985
|
+
const parsed = UserRelationshipInputSchema.parse(args ?? {});
|
|
1986
|
+
const { pagination, order } = buildUserRelationshipParams(parsed);
|
|
1987
|
+
const response = await connpassClient.getUserAttendedEvents(
|
|
1988
|
+
parsed.userId,
|
|
1989
|
+
{ ...pagination, order }
|
|
1990
|
+
);
|
|
1991
|
+
const formatOptions = parsed.includeDetails ? FORMAT_PRESETS.detailed : FORMAT_PRESETS.default;
|
|
1992
|
+
const formatted = formatEventsResponse(response, formatOptions);
|
|
1993
|
+
return {
|
|
1994
|
+
content: [
|
|
1995
|
+
{
|
|
1996
|
+
type: "text",
|
|
1997
|
+
text: summarizeEventsResponse(formatted, "attended events")
|
|
1998
|
+
}
|
|
1999
|
+
]
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
registerAppToolIfEnabled(
|
|
2004
|
+
server2,
|
|
2005
|
+
"get_user_presenter_events",
|
|
2006
|
+
{
|
|
2007
|
+
title: "Get User Presenter Events",
|
|
2008
|
+
description: "List events where the user presented",
|
|
2009
|
+
inputSchema: UserRelationshipInputSchema
|
|
2010
|
+
},
|
|
2011
|
+
async (args) => {
|
|
2012
|
+
const parsed = UserRelationshipInputSchema.parse(args ?? {});
|
|
2013
|
+
const { pagination, order } = buildUserRelationshipParams(parsed);
|
|
2014
|
+
const response = await connpassClient.getUserPresenterEvents(
|
|
2015
|
+
parsed.userId,
|
|
2016
|
+
{ ...pagination, order }
|
|
2017
|
+
);
|
|
2018
|
+
const formatOptions = parsed.includeDetails ? FORMAT_PRESETS.detailed : FORMAT_PRESETS.default;
|
|
2019
|
+
const formatted = formatEventsResponse(response, formatOptions);
|
|
2020
|
+
return {
|
|
2021
|
+
content: [
|
|
2022
|
+
{
|
|
2023
|
+
type: "text",
|
|
2024
|
+
text: summarizeEventsResponse(formatted, "presenter events")
|
|
2025
|
+
}
|
|
2026
|
+
]
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/tools/index.ts
|
|
2033
|
+
function registerAllTools(deps) {
|
|
2034
|
+
registerConnpassResource(deps);
|
|
2035
|
+
registerEventTools(deps);
|
|
2036
|
+
registerScheduleTools(deps);
|
|
2037
|
+
registerGroupTools(deps);
|
|
2038
|
+
registerPrefectureTools(deps);
|
|
2039
|
+
registerUserTools(deps);
|
|
2040
|
+
registerUITools(deps);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/tools/utils/searchSessionStore.ts
|
|
2044
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2045
|
+
var SearchSessionStore = class {
|
|
2046
|
+
ttlMs;
|
|
2047
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2048
|
+
constructor(options = {}) {
|
|
2049
|
+
this.ttlMs = options.ttlMs ?? 10 * 60 * 1e3;
|
|
2050
|
+
}
|
|
2051
|
+
save(result) {
|
|
2052
|
+
this.cleanup();
|
|
2053
|
+
const sessionId = randomUUID2();
|
|
2054
|
+
this.sessions.set(sessionId, {
|
|
2055
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
2056
|
+
result
|
|
2057
|
+
});
|
|
2058
|
+
return sessionId;
|
|
2059
|
+
}
|
|
2060
|
+
get(sessionId) {
|
|
2061
|
+
this.cleanup();
|
|
2062
|
+
const entry = this.sessions.get(sessionId);
|
|
2063
|
+
if (!entry) {
|
|
2064
|
+
return void 0;
|
|
2065
|
+
}
|
|
2066
|
+
if (entry.expiresAt <= Date.now()) {
|
|
2067
|
+
this.sessions.delete(sessionId);
|
|
2068
|
+
return void 0;
|
|
2069
|
+
}
|
|
2070
|
+
return entry.result;
|
|
2071
|
+
}
|
|
2072
|
+
cleanup() {
|
|
2073
|
+
const now = Date.now();
|
|
2074
|
+
for (const [sessionId, entry] of this.sessions.entries()) {
|
|
2075
|
+
if (entry.expiresAt <= now) {
|
|
2076
|
+
this.sessions.delete(sessionId);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// src/server.ts
|
|
2083
|
+
var config = getConfig();
|
|
2084
|
+
var cachedServer = null;
|
|
2085
|
+
function createUnavailableConnpassClient() {
|
|
2086
|
+
const message = "CONNPASS_API_KEY is required to use Connpass API tools. Set the environment variable or pass --connpass-api-key.";
|
|
2087
|
+
return new Proxy({}, {
|
|
2088
|
+
get(_target, property) {
|
|
2089
|
+
if (property === "then") {
|
|
2090
|
+
return void 0;
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof property === "symbol") {
|
|
2093
|
+
return void 0;
|
|
2094
|
+
}
|
|
2095
|
+
return async () => {
|
|
2096
|
+
throw new Error(message);
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
function createConnpassClient() {
|
|
2102
|
+
if (!config.connpassApiKey?.trim()) {
|
|
2103
|
+
return createUnavailableConnpassClient();
|
|
2104
|
+
}
|
|
2105
|
+
return new ConnpassClient({
|
|
2106
|
+
apiKey: config.connpassApiKey,
|
|
2107
|
+
rateLimitEnabled: config.rateLimitEnabled,
|
|
2108
|
+
rateLimitDelay: config.rateLimitDelayMs
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
function createServer() {
|
|
2112
|
+
const server2 = new McpServer({
|
|
2113
|
+
name: "connpass-mcp-server",
|
|
2114
|
+
version: "0.3.0",
|
|
2115
|
+
description: "Connpass event search and browsing MCP server with Apps UI"
|
|
2116
|
+
});
|
|
2117
|
+
const connpassClient = createConnpassClient();
|
|
2118
|
+
const searchSessionStore = new SearchSessionStore();
|
|
2119
|
+
const deps = {
|
|
2120
|
+
server: server2,
|
|
2121
|
+
connpassClient,
|
|
2122
|
+
config,
|
|
2123
|
+
searchSessionStore
|
|
2124
|
+
};
|
|
2125
|
+
registerAllTools(deps);
|
|
2126
|
+
return server2;
|
|
2127
|
+
}
|
|
2128
|
+
function getServer() {
|
|
2129
|
+
if (!cachedServer) {
|
|
2130
|
+
cachedServer = createServer();
|
|
2131
|
+
}
|
|
2132
|
+
return cachedServer;
|
|
2133
|
+
}
|
|
2134
|
+
var server = getServer();
|
|
2135
|
+
|
|
2136
|
+
// src/stdio.ts
|
|
2137
|
+
connectStdio(getServer()).catch(() => {
|
|
2138
|
+
process.exit(1);
|
|
2139
|
+
});
|
|
2140
|
+
//# sourceMappingURL=stdio.js.map
|