@charlie.act7/canvas-mcp-server 1.1.0

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.
@@ -0,0 +1,760 @@
1
+ import Fastify from "fastify";
2
+ import swagger from "@fastify/swagger";
3
+ import swaggerUi from "@fastify/swagger-ui";
4
+ function buildOpenApiSpec() {
5
+ return {
6
+ openapi: "3.1.0",
7
+ info: {
8
+ title: "Canvas MCP HTTP API",
9
+ version: "1.0.0",
10
+ description: "HTTP facade for Canvas operations, suitable for GPT Builder Actions."
11
+ },
12
+ servers: [
13
+ {
14
+ url: "https://mcp-canvas-server.onrender.com"
15
+ }
16
+ ],
17
+ paths: {
18
+ "/health": {
19
+ get: {
20
+ operationId: "health",
21
+ summary: "Health check",
22
+ responses: {
23
+ "200": {
24
+ description: "Server is healthy"
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "/privacy": {
30
+ get: {
31
+ operationId: "getPrivacyPolicy",
32
+ summary: "Privacy policy",
33
+ responses: {
34
+ "200": {
35
+ description: "Privacy policy text"
36
+ }
37
+ }
38
+ }
39
+ },
40
+ "/courses": {
41
+ get: {
42
+ operationId: "listCourses",
43
+ summary: "List active Canvas courses",
44
+ responses: {
45
+ "200": {
46
+ description: "Courses list"
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "/courses/{courseId}/assignments": {
52
+ get: {
53
+ operationId: "listAssignments",
54
+ summary: "List course assignments (compact by default to avoid large responses)",
55
+ parameters: [
56
+ {
57
+ name: "courseId",
58
+ in: "path",
59
+ required: true,
60
+ schema: { type: "integer" }
61
+ },
62
+ {
63
+ name: "search",
64
+ in: "query",
65
+ required: false,
66
+ schema: { type: "string" }
67
+ },
68
+ {
69
+ name: "limit",
70
+ in: "query",
71
+ required: false,
72
+ schema: { type: "integer", default: 50, minimum: 1, maximum: 200 }
73
+ },
74
+ {
75
+ name: "upcomingOnly",
76
+ in: "query",
77
+ required: false,
78
+ schema: { type: "boolean", default: false }
79
+ },
80
+ {
81
+ name: "full",
82
+ in: "query",
83
+ required: false,
84
+ schema: { type: "boolean", default: false }
85
+ }
86
+ ],
87
+ responses: {
88
+ "200": {
89
+ description: "Assignments list"
90
+ }
91
+ }
92
+ }
93
+ },
94
+ "/courses/{courseId}/assignments/{assignmentId}": {
95
+ get: {
96
+ operationId: "getAssignment",
97
+ summary: "Get assignment details",
98
+ parameters: [
99
+ {
100
+ name: "courseId",
101
+ in: "path",
102
+ required: true,
103
+ schema: { type: "integer" }
104
+ },
105
+ {
106
+ name: "assignmentId",
107
+ in: "path",
108
+ required: true,
109
+ schema: { type: "integer" }
110
+ }
111
+ ],
112
+ responses: {
113
+ "200": {
114
+ description: "Assignment details"
115
+ }
116
+ }
117
+ }
118
+ },
119
+ "/courses/{courseId}/assignments/{assignmentId}/dates": {
120
+ patch: {
121
+ operationId: "updateAssignmentDates",
122
+ summary: "Update assignment due/unlock/lock dates",
123
+ parameters: [
124
+ {
125
+ name: "courseId",
126
+ in: "path",
127
+ required: true,
128
+ schema: { type: "integer" }
129
+ },
130
+ {
131
+ name: "assignmentId",
132
+ in: "path",
133
+ required: true,
134
+ schema: { type: "integer" }
135
+ }
136
+ ],
137
+ requestBody: {
138
+ required: true,
139
+ content: {
140
+ "application/json": {
141
+ schema: {
142
+ type: "object",
143
+ properties: {
144
+ due_at: {
145
+ type: ["string", "null"],
146
+ description: "ISO-8601 date. Example: 2026-02-20T23:59:00Z"
147
+ },
148
+ unlock_at: {
149
+ type: ["string", "null"],
150
+ description: "ISO-8601 date or null"
151
+ },
152
+ lock_at: {
153
+ type: ["string", "null"],
154
+ description: "ISO-8601 date or null"
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ },
161
+ responses: {
162
+ "200": {
163
+ description: "Updated assignment"
164
+ },
165
+ "400": {
166
+ description: "Invalid payload"
167
+ }
168
+ }
169
+ }
170
+ },
171
+ "/courses/{courseId}/quizzes": {
172
+ get: {
173
+ operationId: "listQuizzes",
174
+ summary: "List quizzes in a course",
175
+ parameters: [
176
+ {
177
+ name: "courseId",
178
+ in: "path",
179
+ required: true,
180
+ schema: { type: "integer" }
181
+ }
182
+ ],
183
+ responses: {
184
+ "200": {
185
+ description: "Quizzes list"
186
+ }
187
+ }
188
+ }
189
+ },
190
+ "/courses/{courseId}/quizzes/{quizId}": {
191
+ get: {
192
+ operationId: "getQuiz",
193
+ summary: "Get quiz details",
194
+ parameters: [
195
+ {
196
+ name: "courseId",
197
+ in: "path",
198
+ required: true,
199
+ schema: { type: "integer" }
200
+ },
201
+ {
202
+ name: "quizId",
203
+ in: "path",
204
+ required: true,
205
+ schema: { type: "integer" }
206
+ }
207
+ ],
208
+ responses: {
209
+ "200": {
210
+ description: "Quiz details"
211
+ }
212
+ }
213
+ }
214
+ },
215
+ "/courses/{courseId}/quizzes/{quizId}/dates": {
216
+ patch: {
217
+ operationId: "updateQuizDates",
218
+ summary: "Update quiz due/unlock/lock dates",
219
+ parameters: [
220
+ {
221
+ name: "courseId",
222
+ in: "path",
223
+ required: true,
224
+ schema: { type: "integer" }
225
+ },
226
+ {
227
+ name: "quizId",
228
+ in: "path",
229
+ required: true,
230
+ schema: { type: "integer" }
231
+ }
232
+ ],
233
+ requestBody: {
234
+ required: true,
235
+ content: {
236
+ "application/json": {
237
+ schema: {
238
+ type: "object",
239
+ properties: {
240
+ due_at: { type: ["string", "null"] },
241
+ unlock_at: { type: ["string", "null"] },
242
+ lock_at: { type: ["string", "null"] }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ },
248
+ responses: {
249
+ "200": {
250
+ description: "Updated quiz"
251
+ },
252
+ "400": {
253
+ description: "Invalid payload"
254
+ }
255
+ }
256
+ }
257
+ },
258
+ "/courses/{courseId}/quizzes/{quizId}/questions": {
259
+ get: {
260
+ operationId: "listQuizQuestions",
261
+ summary: "List all questions in a quiz",
262
+ parameters: [
263
+ {
264
+ name: "courseId",
265
+ in: "path",
266
+ required: true,
267
+ schema: { type: "integer" }
268
+ },
269
+ {
270
+ name: "quizId",
271
+ in: "path",
272
+ required: true,
273
+ schema: { type: "integer" }
274
+ }
275
+ ],
276
+ responses: {
277
+ "200": {
278
+ description: "Quiz questions list"
279
+ }
280
+ }
281
+ },
282
+ post: {
283
+ operationId: "createQuizQuestion",
284
+ summary: "Create a question in a quiz",
285
+ parameters: [
286
+ {
287
+ name: "courseId",
288
+ in: "path",
289
+ required: true,
290
+ schema: { type: "integer" }
291
+ },
292
+ {
293
+ name: "quizId",
294
+ in: "path",
295
+ required: true,
296
+ schema: { type: "integer" }
297
+ }
298
+ ],
299
+ requestBody: {
300
+ required: true,
301
+ content: {
302
+ "application/json": {
303
+ schema: {
304
+ type: "object",
305
+ properties: {
306
+ question_name: { type: "string", description: "Question name/title" },
307
+ question_type: { type: "string", description: "e.g. multiple_choice_question, true_false_question, essay_question" },
308
+ question_text: { type: "string", description: "Question text (HTML)" },
309
+ points_possible: { type: "number", description: "Point value" },
310
+ quiz_group_id: { type: "integer", description: "Optional quiz group ID" },
311
+ answers: {
312
+ type: "array",
313
+ items: {
314
+ type: "object",
315
+ properties: {
316
+ text: { type: "string" },
317
+ weight: { type: "number" },
318
+ comments: { type: "string" }
319
+ },
320
+ required: ["text", "weight"]
321
+ }
322
+ }
323
+ },
324
+ required: ["question_name", "question_type", "question_text", "points_possible"]
325
+ }
326
+ }
327
+ }
328
+ },
329
+ responses: {
330
+ "201": {
331
+ description: "Created quiz question"
332
+ },
333
+ "400": {
334
+ description: "Invalid payload"
335
+ }
336
+ }
337
+ }
338
+ },
339
+ "/courses/{courseId}/quizzes/{quizId}/questions/{questionId}": {
340
+ put: {
341
+ operationId: "updateQuizQuestion",
342
+ summary: "Update a question in a quiz",
343
+ parameters: [
344
+ {
345
+ name: "courseId",
346
+ in: "path",
347
+ required: true,
348
+ schema: { type: "integer" }
349
+ },
350
+ {
351
+ name: "quizId",
352
+ in: "path",
353
+ required: true,
354
+ schema: { type: "integer" }
355
+ },
356
+ {
357
+ name: "questionId",
358
+ in: "path",
359
+ required: true,
360
+ schema: { type: "integer" }
361
+ }
362
+ ],
363
+ requestBody: {
364
+ required: true,
365
+ content: {
366
+ "application/json": {
367
+ schema: {
368
+ type: "object",
369
+ properties: {
370
+ question_name: { type: "string" },
371
+ question_type: { type: "string" },
372
+ question_text: { type: "string" },
373
+ points_possible: { type: "number" },
374
+ quiz_group_id: { type: ["integer", "null"] },
375
+ answers: {
376
+ type: "array",
377
+ items: {
378
+ type: "object",
379
+ properties: {
380
+ text: { type: "string" },
381
+ weight: { type: "number" },
382
+ comments: { type: "string" }
383
+ },
384
+ required: ["text", "weight"]
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+ },
392
+ responses: {
393
+ "200": {
394
+ description: "Updated quiz question"
395
+ },
396
+ "400": {
397
+ description: "Invalid payload"
398
+ }
399
+ }
400
+ },
401
+ delete: {
402
+ operationId: "deleteQuizQuestion",
403
+ summary: "Delete a question from a quiz",
404
+ parameters: [
405
+ {
406
+ name: "courseId",
407
+ in: "path",
408
+ required: true,
409
+ schema: { type: "integer" }
410
+ },
411
+ {
412
+ name: "quizId",
413
+ in: "path",
414
+ required: true,
415
+ schema: { type: "integer" }
416
+ },
417
+ {
418
+ name: "questionId",
419
+ in: "path",
420
+ required: true,
421
+ schema: { type: "integer" }
422
+ }
423
+ ],
424
+ responses: {
425
+ "200": {
426
+ description: "Deletion result"
427
+ }
428
+ }
429
+ }
430
+ },
431
+ "/courses/{courseId}/quizzes/{quizId}/groups": {
432
+ post: {
433
+ operationId: "createQuizGroup",
434
+ summary: "Create a quiz group linked to a question bank",
435
+ parameters: [
436
+ {
437
+ name: "courseId",
438
+ in: "path",
439
+ required: true,
440
+ schema: { type: "integer" }
441
+ },
442
+ {
443
+ name: "quizId",
444
+ in: "path",
445
+ required: true,
446
+ schema: { type: "integer" }
447
+ }
448
+ ],
449
+ requestBody: {
450
+ required: true,
451
+ content: {
452
+ "application/json": {
453
+ schema: {
454
+ type: "object",
455
+ properties: {
456
+ name: { type: "string", description: "Group name" },
457
+ pick_count: { type: "integer", description: "Number of questions to pick" },
458
+ question_points: { type: "number", description: "Points per question" },
459
+ assessment_question_bank_id: { type: "integer", description: "Question bank to link" }
460
+ },
461
+ required: ["name", "pick_count", "question_points"]
462
+ }
463
+ }
464
+ }
465
+ },
466
+ responses: {
467
+ "201": {
468
+ description: "Created quiz group"
469
+ },
470
+ "400": {
471
+ description: "Invalid payload"
472
+ }
473
+ }
474
+ }
475
+ },
476
+ "/courses/{courseId}/assignments/bulk-due-date": {
477
+ patch: {
478
+ operationId: "bulkUpdateAssignmentDueDateByQuery",
479
+ summary: "Update due date for assignments matched by query terms in their names",
480
+ parameters: [
481
+ {
482
+ name: "courseId",
483
+ in: "path",
484
+ required: true,
485
+ schema: { type: "integer" }
486
+ }
487
+ ],
488
+ requestBody: {
489
+ required: true,
490
+ content: {
491
+ "application/json": {
492
+ schema: {
493
+ type: "object",
494
+ properties: {
495
+ query_terms: {
496
+ type: "array",
497
+ items: { type: "string" },
498
+ description: "All terms must appear in assignment name (case-insensitive)"
499
+ },
500
+ due_at: {
501
+ type: "string",
502
+ description: "ISO-8601 due date to apply (e.g., 2026-02-12T23:59:00-05:00)"
503
+ },
504
+ limit: {
505
+ type: "integer",
506
+ default: 20,
507
+ minimum: 1,
508
+ maximum: 100
509
+ },
510
+ dry_run: {
511
+ type: "boolean",
512
+ default: false
513
+ }
514
+ },
515
+ required: ["query_terms", "due_at"]
516
+ }
517
+ }
518
+ }
519
+ },
520
+ responses: {
521
+ "200": {
522
+ description: "Bulk update result"
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ };
529
+ }
530
+ export async function startHttpServer(client, host = "0.0.0.0", port = 3000) {
531
+ const app = Fastify({ logger: true });
532
+ await app.register(swagger, {
533
+ openapi: buildOpenApiSpec()
534
+ });
535
+ await app.register(swaggerUi, {
536
+ routePrefix: "/docs"
537
+ });
538
+ app.get("/health", async () => ({ ok: true }));
539
+ app.get("/privacy", async () => ({
540
+ service: "Canvas MCP HTTP API",
541
+ effective_date: "2026-02-12",
542
+ summary: [
543
+ "This service processes Canvas API data strictly to fulfill user requests.",
544
+ "Canvas API tokens are provided via environment variables and are not exposed in API responses.",
545
+ "Do not send sensitive data beyond what is required for course operations."
546
+ ],
547
+ contact: "Set a maintainer contact before production use."
548
+ }));
549
+ app.get("/openapi.json", async () => buildOpenApiSpec());
550
+ app.get("/courses", async () => {
551
+ return client.getCourses();
552
+ });
553
+ app.get("/courses/:courseId/assignments", async (request) => {
554
+ const courseId = Number.parseInt(request.params.courseId, 10);
555
+ const search = (request.query.search || "").toLowerCase().trim();
556
+ const upcomingOnly = request.query.upcomingOnly === "true";
557
+ const full = request.query.full === "true";
558
+ const requestedLimit = Number.parseInt(request.query.limit || "50", 10);
559
+ const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(requestedLimit, 1), 200) : 50;
560
+ const assignments = await client.getAssignments(courseId);
561
+ const now = new Date();
562
+ const filtered = assignments.filter((a) => {
563
+ const matchesSearch = !search || (a.name || "").toLowerCase().includes(search);
564
+ if (!matchesSearch)
565
+ return false;
566
+ if (!upcomingOnly)
567
+ return true;
568
+ if (!a.due_at)
569
+ return false;
570
+ return new Date(a.due_at) >= now;
571
+ });
572
+ if (full) {
573
+ return filtered.slice(0, limit);
574
+ }
575
+ return filtered.slice(0, limit).map((a) => ({
576
+ id: a.id ?? null,
577
+ name: a.name,
578
+ due_at: a.due_at ?? null,
579
+ unlock_at: a.unlock_at ?? null,
580
+ lock_at: a.lock_at ?? null,
581
+ points_possible: a.points_possible ?? null,
582
+ published: a.published ?? null
583
+ }));
584
+ });
585
+ app.get("/courses/:courseId/assignments/:assignmentId", async (request) => {
586
+ const courseId = Number.parseInt(request.params.courseId, 10);
587
+ const assignmentId = Number.parseInt(request.params.assignmentId, 10);
588
+ return client.getAssignment(courseId, assignmentId);
589
+ });
590
+ app.patch("/courses/:courseId/assignments/:assignmentId/dates", async (request, reply) => {
591
+ const { due_at, unlock_at, lock_at } = request.body || {};
592
+ if (due_at === undefined && unlock_at === undefined && lock_at === undefined) {
593
+ return reply.code(400).send({
594
+ error: "At least one date field is required: due_at, unlock_at, or lock_at."
595
+ });
596
+ }
597
+ const courseId = Number.parseInt(request.params.courseId, 10);
598
+ const assignmentId = Number.parseInt(request.params.assignmentId, 10);
599
+ return client.updateAssignmentDates(courseId, assignmentId, {
600
+ due_at,
601
+ unlock_at,
602
+ lock_at
603
+ });
604
+ });
605
+ app.get("/courses/:courseId/quizzes", async (request) => {
606
+ const courseId = Number.parseInt(request.params.courseId, 10);
607
+ return client.getQuizzes(courseId);
608
+ });
609
+ app.get("/courses/:courseId/quizzes/:quizId", async (request) => {
610
+ const courseId = Number.parseInt(request.params.courseId, 10);
611
+ const quizId = Number.parseInt(request.params.quizId, 10);
612
+ return client.getQuiz(courseId, quizId);
613
+ });
614
+ app.patch("/courses/:courseId/quizzes/:quizId/dates", async (request, reply) => {
615
+ const { due_at, unlock_at, lock_at } = request.body || {};
616
+ if (due_at === undefined && unlock_at === undefined && lock_at === undefined) {
617
+ return reply.code(400).send({
618
+ error: "At least one date field is required: due_at, unlock_at, or lock_at."
619
+ });
620
+ }
621
+ const courseId = Number.parseInt(request.params.courseId, 10);
622
+ const quizId = Number.parseInt(request.params.quizId, 10);
623
+ return client.updateQuizDates(courseId, quizId, {
624
+ due_at,
625
+ unlock_at,
626
+ lock_at
627
+ });
628
+ });
629
+ // --- Quiz Questions ---
630
+ app.get("/courses/:courseId/quizzes/:quizId/questions", async (request) => {
631
+ const courseId = Number.parseInt(request.params.courseId, 10);
632
+ const quizId = Number.parseInt(request.params.quizId, 10);
633
+ return client.listQuizQuestions(courseId, quizId);
634
+ });
635
+ app.post("/courses/:courseId/quizzes/:quizId/questions", async (request, reply) => {
636
+ const { question_name, question_type, question_text, points_possible, quiz_group_id, answers } = request.body || {};
637
+ if (!question_name || !question_type || !question_text || points_possible === undefined) {
638
+ return reply.code(400).send({
639
+ error: "question_name, question_type, question_text and points_possible are required."
640
+ });
641
+ }
642
+ const courseId = Number.parseInt(request.params.courseId, 10);
643
+ const quizId = Number.parseInt(request.params.quizId, 10);
644
+ const question = await client.createQuizQuestion(courseId, quizId, {
645
+ question_name,
646
+ question_type,
647
+ question_text,
648
+ points_possible,
649
+ quiz_group_id,
650
+ answers
651
+ });
652
+ return reply.code(201).send(question);
653
+ });
654
+ app.put("/courses/:courseId/quizzes/:quizId/questions/:questionId", async (request) => {
655
+ const courseId = Number.parseInt(request.params.courseId, 10);
656
+ const quizId = Number.parseInt(request.params.quizId, 10);
657
+ const questionId = Number.parseInt(request.params.questionId, 10);
658
+ const { question_name, question_type, question_text, points_possible, quiz_group_id, answers } = request.body || {};
659
+ const data = {};
660
+ if (question_name !== undefined)
661
+ data.question_name = question_name;
662
+ if (question_type !== undefined)
663
+ data.question_type = question_type;
664
+ if (question_text !== undefined)
665
+ data.question_text = question_text;
666
+ if (points_possible !== undefined)
667
+ data.points_possible = points_possible;
668
+ if (quiz_group_id !== undefined)
669
+ data.quiz_group_id = quiz_group_id;
670
+ if (answers !== undefined)
671
+ data.answers = answers;
672
+ return client.updateQuizQuestion(courseId, quizId, questionId, data);
673
+ });
674
+ app.delete("/courses/:courseId/quizzes/:quizId/questions/:questionId", async (request) => {
675
+ const courseId = Number.parseInt(request.params.courseId, 10);
676
+ const quizId = Number.parseInt(request.params.quizId, 10);
677
+ const questionId = Number.parseInt(request.params.questionId, 10);
678
+ return client.deleteQuizQuestion(courseId, quizId, questionId);
679
+ });
680
+ app.post("/courses/:courseId/quizzes/:quizId/groups", async (request, reply) => {
681
+ const { name, pick_count, question_points, assessment_question_bank_id } = request.body || {};
682
+ if (!name || pick_count === undefined || question_points === undefined) {
683
+ return reply.code(400).send({
684
+ error: "name, pick_count and question_points are required."
685
+ });
686
+ }
687
+ const courseId = Number.parseInt(request.params.courseId, 10);
688
+ const quizId = Number.parseInt(request.params.quizId, 10);
689
+ const group = await client.createQuizGroup(courseId, quizId, {
690
+ name,
691
+ pick_count,
692
+ question_points,
693
+ assessment_question_bank_id
694
+ });
695
+ return reply.code(201).send(group);
696
+ });
697
+ app.patch("/courses/:courseId/assignments/bulk-due-date", async (request, reply) => {
698
+ const courseId = Number.parseInt(request.params.courseId, 10);
699
+ const terms = (request.body.query_terms || []).map((t) => t.toLowerCase().trim()).filter(Boolean);
700
+ const dueAt = request.body.due_at;
701
+ const dryRun = request.body.dry_run === true;
702
+ const rawLimit = request.body.limit ?? 20;
703
+ const limit = Math.min(Math.max(rawLimit, 1), 100);
704
+ if (!terms.length || !dueAt) {
705
+ return reply.code(400).send({
706
+ error: "query_terms and due_at are required."
707
+ });
708
+ }
709
+ const assignments = await client.getAssignments(courseId);
710
+ const matched = assignments
711
+ .filter((a) => {
712
+ const name = (a.name || "").toLowerCase();
713
+ return terms.every((term) => name.includes(term));
714
+ })
715
+ .slice(0, limit);
716
+ const results = [];
717
+ for (const assignment of matched) {
718
+ if (!assignment.id)
719
+ continue;
720
+ if (dryRun) {
721
+ results.push({
722
+ assignment_id: assignment.id,
723
+ assignment_name: assignment.name,
724
+ old_due_at: assignment.due_at ?? null,
725
+ new_due_at: dueAt,
726
+ status: "matched_only"
727
+ });
728
+ continue;
729
+ }
730
+ try {
731
+ const updated = await client.updateAssignmentDates(courseId, assignment.id, { due_at: dueAt });
732
+ results.push({
733
+ assignment_id: updated.id ?? assignment.id,
734
+ assignment_name: updated.name,
735
+ old_due_at: assignment.due_at ?? null,
736
+ new_due_at: updated.due_at ?? dueAt,
737
+ status: "updated"
738
+ });
739
+ }
740
+ catch (error) {
741
+ results.push({
742
+ assignment_id: assignment.id,
743
+ assignment_name: assignment.name,
744
+ old_due_at: assignment.due_at ?? null,
745
+ new_due_at: dueAt,
746
+ status: "error",
747
+ error: error?.message || "Unknown error"
748
+ });
749
+ }
750
+ }
751
+ return {
752
+ course_id: courseId,
753
+ matched_count: matched.length,
754
+ updated_count: results.filter((r) => r.status === "updated").length,
755
+ dry_run: dryRun,
756
+ results
757
+ };
758
+ });
759
+ await app.listen({ host, port });
760
+ }