@mcp-ts/sdk 1.6.1 → 2.0.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.
Files changed (114) hide show
  1. package/README.md +12 -6
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-adapter.js +4 -5
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +4 -5
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +3 -3
  9. package/dist/adapters/agui-middleware.d.ts +3 -3
  10. package/dist/adapters/ai-adapter.d.mts +9 -3
  11. package/dist/adapters/ai-adapter.d.ts +9 -3
  12. package/dist/adapters/ai-adapter.js +20 -6
  13. package/dist/adapters/ai-adapter.js.map +1 -1
  14. package/dist/adapters/ai-adapter.mjs +20 -6
  15. package/dist/adapters/ai-adapter.mjs.map +1 -1
  16. package/dist/adapters/langchain-adapter.d.mts +3 -3
  17. package/dist/adapters/langchain-adapter.d.ts +3 -3
  18. package/dist/adapters/langchain-adapter.js +9 -6
  19. package/dist/adapters/langchain-adapter.js.map +1 -1
  20. package/dist/adapters/langchain-adapter.mjs +9 -6
  21. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  22. package/dist/adapters/mastra-adapter.d.mts +1 -1
  23. package/dist/adapters/mastra-adapter.d.ts +1 -1
  24. package/dist/adapters/mastra-adapter.js +5 -1
  25. package/dist/adapters/mastra-adapter.js.map +1 -1
  26. package/dist/adapters/mastra-adapter.mjs +5 -1
  27. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  28. package/dist/bin/mcp-ts.js +7 -1
  29. package/dist/bin/mcp-ts.js.map +1 -1
  30. package/dist/bin/mcp-ts.mjs +7 -1
  31. package/dist/bin/mcp-ts.mjs.map +1 -1
  32. package/dist/client/index.d.mts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +9 -13
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/index.mjs +9 -13
  37. package/dist/client/index.mjs.map +1 -1
  38. package/dist/client/react.d.mts +7 -7
  39. package/dist/client/react.d.ts +7 -7
  40. package/dist/client/react.js +111 -63
  41. package/dist/client/react.js.map +1 -1
  42. package/dist/client/react.mjs +111 -63
  43. package/dist/client/react.mjs.map +1 -1
  44. package/dist/client/vue.d.mts +7 -7
  45. package/dist/client/vue.d.ts +7 -7
  46. package/dist/client/vue.js +14 -18
  47. package/dist/client/vue.js.map +1 -1
  48. package/dist/client/vue.mjs +14 -18
  49. package/dist/client/vue.mjs.map +1 -1
  50. package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
  51. package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
  52. package/dist/index.d.mts +6 -6
  53. package/dist/index.d.ts +6 -6
  54. package/dist/index.js +616 -370
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +615 -370
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
  59. package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
  60. package/dist/server/index.d.mts +31 -34
  61. package/dist/server/index.d.ts +31 -34
  62. package/dist/server/index.js +531 -256
  63. package/dist/server/index.js.map +1 -1
  64. package/dist/server/index.mjs +530 -256
  65. package/dist/server/index.mjs.map +1 -1
  66. package/dist/shared/index.d.mts +5 -5
  67. package/dist/shared/index.d.ts +5 -5
  68. package/dist/shared/index.js +76 -101
  69. package/dist/shared/index.js.map +1 -1
  70. package/dist/shared/index.mjs +76 -101
  71. package/dist/shared/index.mjs.map +1 -1
  72. package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
  73. package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
  74. package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
  75. package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
  76. package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
  77. package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
  78. package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
  79. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  80. package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
  81. package/package.json +14 -5
  82. package/src/adapters/ai-adapter.ts +30 -1
  83. package/src/adapters/langchain-adapter.ts +6 -2
  84. package/src/adapters/mastra-adapter.ts +6 -2
  85. package/src/bin/mcp-ts.ts +8 -1
  86. package/src/client/core/app-host.ts +1 -1
  87. package/src/client/core/sse-client.ts +12 -14
  88. package/src/client/core/types.ts +1 -1
  89. package/src/client/react/oauth-popup.tsx +111 -51
  90. package/src/client/react/use-mcp-apps.tsx +1 -1
  91. package/src/client/react/use-mcp.ts +11 -11
  92. package/src/client/vue/use-mcp.ts +10 -10
  93. package/src/server/handlers/nextjs-handler.ts +18 -15
  94. package/src/server/handlers/sse-handler.ts +29 -29
  95. package/src/server/index.ts +1 -1
  96. package/src/server/mcp/multi-session-client.ts +17 -17
  97. package/src/server/mcp/oauth-client.ts +37 -37
  98. package/src/server/mcp/storage-oauth-provider.ts +17 -17
  99. package/src/server/storage/file-backend.ts +25 -25
  100. package/src/server/storage/index.ts +67 -10
  101. package/src/server/storage/memory-backend.ts +34 -34
  102. package/src/server/storage/neon-backend.ts +281 -0
  103. package/src/server/storage/redis-backend.ts +64 -64
  104. package/src/server/storage/sqlite-backend.ts +33 -33
  105. package/src/server/storage/supabase-backend.ts +23 -24
  106. package/src/server/storage/types.ts +18 -21
  107. package/src/shared/errors.ts +1 -1
  108. package/src/shared/index.ts +1 -2
  109. package/src/shared/meta-tools.ts +4 -6
  110. package/src/shared/schema-compressor.ts +2 -42
  111. package/src/shared/tool-index.ts +89 -84
  112. package/src/shared/tool-router.ts +0 -24
  113. package/src/shared/types.ts +4 -4
  114. /package/{supabase/migrations → migrations/supabase}/20260421010000_add_session_cleanup_cron.sql +0 -0
@@ -1,16 +1,16 @@
1
- import { StorageBackend, SessionData, SetClientOptions } from './types.js';
1
+ import type { SessionStore, Session, SetClientOptions } from './types.js';
2
2
  import { generateSessionId } from '../../shared/utils.js';
3
3
 
4
4
  /**
5
- * In-memory implementation of StorageBackend
5
+ * In-memory implementation of SessionStore
6
6
  * Useful for local development or testing
7
7
  */
8
- export class MemoryStorageBackend implements StorageBackend {
9
- // Map<identity:sessionId, SessionData>
10
- private sessions = new Map<string, SessionData>();
8
+ export class MemoryStorageBackend implements SessionStore {
9
+ // Map<userId:sessionId, Session>
10
+ private sessions = new Map<string, Session>();
11
11
 
12
- // Map<identity, Set<sessionId>>
13
- private identitySessions = new Map<string, Set<string>>();
12
+ // Map<userId, Set<sessionId>>
13
+ private userIdSessions = new Map<string, Set<string>>();
14
14
 
15
15
  constructor() { }
16
16
 
@@ -18,19 +18,19 @@ export class MemoryStorageBackend implements StorageBackend {
18
18
  console.log('[mcp-ts][Storage] Memory: ✓ internal memory store active.');
19
19
  }
20
20
 
21
- private getSessionKey(identity: string, sessionId: string): string {
22
- return `${identity}:${sessionId}`;
21
+ private getSessionKey(userId: string, sessionId: string): string {
22
+ return `${userId}:${sessionId}`;
23
23
  }
24
24
 
25
25
  generateSessionId(): string {
26
26
  return generateSessionId();
27
27
  }
28
28
 
29
- async createSession(session: SessionData, ttl?: number): Promise<void> {
30
- const { sessionId, identity } = session;
31
- if (!sessionId || !identity) throw new Error('identity and sessionId required');
29
+ async create(session: Session, ttl?: number): Promise<void> {
30
+ const { sessionId, userId } = session;
31
+ if (!sessionId || !userId) throw new Error('userId and sessionId required');
32
32
 
33
- const sessionKey = this.getSessionKey(identity, sessionId);
33
+ const sessionKey = this.getSessionKey(userId, sessionId);
34
34
  if (this.sessions.has(sessionKey)) {
35
35
  throw new Error(`Session ${sessionId} already exists`);
36
36
  }
@@ -38,17 +38,17 @@ export class MemoryStorageBackend implements StorageBackend {
38
38
  this.sessions.set(sessionKey, session);
39
39
 
40
40
  // Update index
41
- if (!this.identitySessions.has(identity)) {
42
- this.identitySessions.set(identity, new Set());
41
+ if (!this.userIdSessions.has(userId)) {
42
+ this.userIdSessions.set(userId, new Set());
43
43
  }
44
- this.identitySessions.get(identity)!.add(sessionId);
44
+ this.userIdSessions.get(userId)!.add(sessionId);
45
45
  // Note: TTL is ignored in memory backend - sessions don't auto-expire
46
46
  }
47
47
 
48
- async updateSession(identity: string, sessionId: string, data: Partial<SessionData>, ttl?: number): Promise<void> {
49
- if (!identity || !sessionId) throw new Error('identity and sessionId required');
48
+ async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
49
+ if (!userId || !sessionId) throw new Error('userId and sessionId required');
50
50
 
51
- const sessionKey = this.getSessionKey(identity, sessionId);
51
+ const sessionKey = this.getSessionKey(userId, sessionId);
52
52
  const current = this.sessions.get(sessionKey);
53
53
 
54
54
  if (!current) {
@@ -65,23 +65,23 @@ export class MemoryStorageBackend implements StorageBackend {
65
65
  }
66
66
 
67
67
 
68
- async getSession(identity: string, sessionId: string): Promise<SessionData | null> {
69
- const sessionKey = this.getSessionKey(identity, sessionId);
68
+ async get(userId: string, sessionId: string): Promise<Session | null> {
69
+ const sessionKey = this.getSessionKey(userId, sessionId);
70
70
  return this.sessions.get(sessionKey) || null;
71
71
  }
72
72
 
73
- async getIdentityMcpSessions(identity: string): Promise<string[]> {
74
- const set = this.identitySessions.get(identity);
73
+ async listIds(userId: string): Promise<string[]> {
74
+ const set = this.userIdSessions.get(userId);
75
75
  return set ? Array.from(set) : [];
76
76
  }
77
77
 
78
- async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
79
- const set = this.identitySessions.get(identity);
78
+ async list(userId: string): Promise<Session[]> {
79
+ const set = this.userIdSessions.get(userId);
80
80
  if (!set) return [];
81
81
 
82
- const results: SessionData[] = [];
82
+ const results: Session[] = [];
83
83
  for (const sessionId of set) {
84
- const session = this.sessions.get(this.getSessionKey(identity, sessionId));
84
+ const session = this.sessions.get(this.getSessionKey(userId, sessionId));
85
85
  if (session) {
86
86
  results.push(session);
87
87
  }
@@ -89,29 +89,29 @@ export class MemoryStorageBackend implements StorageBackend {
89
89
  return results;
90
90
  }
91
91
 
92
- async removeSession(identity: string, sessionId: string): Promise<void> {
93
- const sessionKey = this.getSessionKey(identity, sessionId);
92
+ async delete(userId: string, sessionId: string): Promise<void> {
93
+ const sessionKey = this.getSessionKey(userId, sessionId);
94
94
  this.sessions.delete(sessionKey);
95
95
 
96
- const set = this.identitySessions.get(identity);
96
+ const set = this.userIdSessions.get(userId);
97
97
  if (set) {
98
98
  set.delete(sessionId);
99
99
  if (set.size === 0) {
100
- this.identitySessions.delete(identity);
100
+ this.userIdSessions.delete(userId);
101
101
  }
102
102
  }
103
103
  }
104
104
 
105
- async getAllSessionIds(): Promise<string[]> {
105
+ async listAllIds(): Promise<string[]> {
106
106
  return Array.from(this.sessions.values()).map(s => s.sessionId);
107
107
  }
108
108
 
109
109
  async clearAll(): Promise<void> {
110
110
  this.sessions.clear();
111
- this.identitySessions.clear();
111
+ this.userIdSessions.clear();
112
112
  }
113
113
 
114
- async cleanupExpiredSessions(): Promise<void> {
114
+ async cleanupExpired(): Promise<void> {
115
115
  // In-memory doesn't implement TTL automatically,
116
116
  // but we could check createdAt + TTL here if needed.
117
117
  // For now, no-op.
@@ -0,0 +1,281 @@
1
+ import type { SessionStore, Session } from './types.js';
2
+ import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
3
+ import { generateSessionId } from '../../shared/utils.js';
4
+ import { encryptObject, decryptObject } from './crypto.js';
5
+
6
+ export interface NeonStorageOptions {
7
+ schema?: string;
8
+ table?: string;
9
+ }
10
+
11
+ type NeonSql = {
12
+ query(queryWithPlaceholders: string, params?: unknown[]): Promise<any[]>;
13
+ };
14
+
15
+ type NeonSessionRow = {
16
+ session_id: string;
17
+ server_id?: string | null;
18
+ server_name?: string | null;
19
+ server_url: string;
20
+ transport_type: 'sse' | 'streamable-http';
21
+ callback_url: string;
22
+ created_at: string | Date;
23
+ user_id: string;
24
+ headers?: unknown;
25
+ active?: boolean | null;
26
+ client_information?: unknown;
27
+ tokens?: unknown;
28
+ code_verifier?: string | null;
29
+ client_id?: string | null;
30
+ };
31
+
32
+ export class NeonStorageBackend implements SessionStore {
33
+ private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
34
+ private readonly tableName: string;
35
+
36
+ constructor(
37
+ private readonly sql: NeonSql,
38
+ options: NeonStorageOptions = {}
39
+ ) {
40
+ const schema = options.schema || 'public';
41
+ const table = options.table || 'mcp_sessions';
42
+ this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
43
+ }
44
+
45
+ async init(): Promise<void> {
46
+ const [{ exists } = { exists: null }] = await this.sql.query(
47
+ 'SELECT to_regclass($1) AS exists',
48
+ [this.tableName.replace(/"/g, '')]
49
+ ) as Array<{ exists: string | null }>;
50
+
51
+ if (!exists) {
52
+ throw new Error(
53
+ '[NeonStorage] Table "mcp_sessions" not found in your database. ' +
54
+ 'Please create it using the Neon storage guide in docs/storage-backends/neon.md.'
55
+ );
56
+ }
57
+
58
+ console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
59
+ }
60
+
61
+ generateSessionId(): string {
62
+ return generateSessionId();
63
+ }
64
+
65
+ private quoteIdentifier(identifier: string): string {
66
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
67
+ throw new Error(`Invalid Neon storage identifier: ${identifier}`);
68
+ }
69
+ return `"${identifier}"`;
70
+ }
71
+
72
+ private mapRowToSessionData(row: NeonSessionRow): Session {
73
+ return {
74
+ sessionId: row.session_id,
75
+ serverId: row.server_id ?? undefined,
76
+ serverName: row.server_name ?? undefined,
77
+ serverUrl: row.server_url,
78
+ transportType: row.transport_type,
79
+ callbackUrl: row.callback_url,
80
+ createdAt: new Date(row.created_at).getTime(),
81
+ userId: row.user_id,
82
+ headers: decryptObject(row.headers),
83
+ active: row.active ?? false,
84
+ clientInformation: row.client_information as Session['clientInformation'],
85
+ tokens: decryptObject(row.tokens),
86
+ codeVerifier: row.code_verifier ?? undefined,
87
+ clientId: row.client_id ?? undefined,
88
+ };
89
+ }
90
+
91
+ async create(session: Session, ttl?: number): Promise<void> {
92
+ const { sessionId, userId } = session;
93
+ if (!sessionId || !userId) throw new Error('userId and sessionId required');
94
+
95
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
96
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
97
+
98
+ try {
99
+ await this.sql.query(
100
+ `INSERT INTO ${this.tableName} (
101
+ session_id,
102
+ user_id,
103
+ server_id,
104
+ server_name,
105
+ server_url,
106
+ transport_type,
107
+ callback_url,
108
+ created_at,
109
+ headers,
110
+ active,
111
+ client_information,
112
+ tokens,
113
+ code_verifier,
114
+ client_id,
115
+ expires_at
116
+ ) VALUES (
117
+ $1, $2, $3, $4, $5, $6, $7, $8,
118
+ $9, $10, $11, $12, $13, $14, $15
119
+ )`,
120
+ [
121
+ sessionId,
122
+ userId,
123
+ session.serverId,
124
+ session.serverName,
125
+ session.serverUrl,
126
+ session.transportType,
127
+ session.callbackUrl,
128
+ new Date(session.createdAt || Date.now()).toISOString(),
129
+ encryptObject(session.headers),
130
+ session.active ?? false,
131
+ session.clientInformation,
132
+ encryptObject(session.tokens),
133
+ session.codeVerifier,
134
+ session.clientId,
135
+ expiresAt,
136
+ ]
137
+ );
138
+ } catch (error: any) {
139
+ if (error.code === '23505') {
140
+ throw new Error(`Session ${sessionId} already exists`);
141
+ }
142
+ throw new Error(`Failed to create session in Neon: ${error.message}`);
143
+ }
144
+ }
145
+
146
+ async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
147
+ const currentSession = await this.get(userId, sessionId);
148
+ if (!currentSession) {
149
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
150
+ }
151
+
152
+ const updatedSession = { ...currentSession, ...data };
153
+ const effectiveTtl = ttl ?? this.DEFAULT_TTL;
154
+ const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
155
+
156
+ const updatedRows = await this.sql.query(
157
+ `UPDATE ${this.tableName}
158
+ SET
159
+ server_id = $1,
160
+ server_name = $2,
161
+ server_url = $3,
162
+ transport_type = $4,
163
+ callback_url = $5,
164
+ active = $6,
165
+ headers = $7,
166
+ client_information = $8,
167
+ tokens = $9,
168
+ code_verifier = $10,
169
+ client_id = $11,
170
+ expires_at = $12,
171
+ updated_at = now()
172
+ WHERE user_id = $13 AND session_id = $14
173
+ RETURNING id`,
174
+ [
175
+ updatedSession.serverId,
176
+ updatedSession.serverName,
177
+ updatedSession.serverUrl,
178
+ updatedSession.transportType,
179
+ updatedSession.callbackUrl,
180
+ updatedSession.active ?? false,
181
+ encryptObject(updatedSession.headers),
182
+ updatedSession.clientInformation,
183
+ encryptObject(updatedSession.tokens),
184
+ updatedSession.codeVerifier,
185
+ updatedSession.clientId,
186
+ expiresAt,
187
+ userId,
188
+ sessionId,
189
+ ]
190
+ ) as Array<{ id: string }>;
191
+
192
+ if (updatedRows.length === 0) {
193
+ throw new Error(`Session ${sessionId} not found for userId ${userId}`);
194
+ }
195
+ }
196
+
197
+ async get(userId: string, sessionId: string): Promise<Session | null> {
198
+ try {
199
+ const rows = await this.sql.query(
200
+ `SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
201
+ [userId, sessionId]
202
+ ) as NeonSessionRow[];
203
+ return rows[0] ? this.mapRowToSessionData(rows[0]) : null;
204
+ } catch (error) {
205
+ console.error('[NeonStorage] Failed to get session:', error);
206
+ return null;
207
+ }
208
+ }
209
+
210
+ async list(userId: string): Promise<Session[]> {
211
+ try {
212
+ const rows = await this.sql.query(
213
+ `SELECT * FROM ${this.tableName} WHERE user_id = $1`,
214
+ [userId]
215
+ ) as NeonSessionRow[];
216
+ return rows.map((row) => this.mapRowToSessionData(row));
217
+ } catch (error) {
218
+ console.error(`[NeonStorage] Failed to get session data for ${userId}:`, error);
219
+ return [];
220
+ }
221
+ }
222
+
223
+ async delete(userId: string, sessionId: string): Promise<void> {
224
+ try {
225
+ await this.sql.query(
226
+ `DELETE FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
227
+ [userId, sessionId]
228
+ );
229
+ } catch (error) {
230
+ console.error('[NeonStorage] Failed to remove session:', error);
231
+ }
232
+ }
233
+
234
+ async listIds(userId: string): Promise<string[]> {
235
+ try {
236
+ const rows = await this.sql.query(
237
+ `SELECT session_id FROM ${this.tableName} WHERE user_id = $1`,
238
+ [userId]
239
+ ) as Array<{ session_id: string }>;
240
+ return rows.map((row) => row.session_id);
241
+ } catch (error) {
242
+ console.error(`[NeonStorage] Failed to get sessions for ${userId}:`, error);
243
+ return [];
244
+ }
245
+ }
246
+
247
+ async listAllIds(): Promise<string[]> {
248
+ try {
249
+ const rows = await this.sql.query(
250
+ `SELECT session_id FROM ${this.tableName}`
251
+ ) as Array<{ session_id: string }>;
252
+ return rows.map((row) => row.session_id);
253
+ } catch (error) {
254
+ console.error('[NeonStorage] Failed to get all sessions:', error);
255
+ return [];
256
+ }
257
+ }
258
+
259
+ async clearAll(): Promise<void> {
260
+ try {
261
+ await this.sql.query(`DELETE FROM ${this.tableName}`);
262
+ } catch (error) {
263
+ console.error('[NeonStorage] Failed to clear sessions:', error);
264
+ }
265
+ }
266
+
267
+ async cleanupExpired(): Promise<void> {
268
+ try {
269
+ await this.sql.query(
270
+ `DELETE FROM ${this.tableName} WHERE expires_at < $1`,
271
+ [new Date().toISOString()]
272
+ );
273
+ } catch (error) {
274
+ console.error('[NeonStorage] Failed to cleanup expired sessions:', error);
275
+ }
276
+ }
277
+
278
+ async disconnect(): Promise<void> {
279
+ // Neon HTTP queries do not hold a persistent connection.
280
+ }
281
+ }