@ouraihub/aether-worker 0.1.0 → 0.1.2

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.
@@ -1,15 +1,22 @@
1
1
  import type { ImagePart, ImageRef } from '@ouraihub/aether-contracts'
2
- import { generateImage, ImageFailure, type ImageConnection, type ImageRequest } from '@ouraihub/aether-core/images'
2
+ import {
3
+ generateImage,
4
+ ImageFailure,
5
+ type ImageConnection,
6
+ type ImageRequest,
7
+ type ImageGeneration,
8
+ type ImageLimits,
9
+ } from '@ouraihub/aether-core/images'
3
10
  import type { OutboundHttp } from '@ouraihub/aether-core/ports'
4
11
  import type { WorkerContext } from '../context.js'
5
12
 
6
- export interface GeneratedImage {
7
- readonly image: ImagePart
13
+ export interface GeneratedImage extends ImageGeneration {
8
14
  readonly reference: ImageRef
9
15
  readonly persisted: boolean
10
16
  }
11
17
  export interface ImageServiceOptions {
12
18
  readonly outbound: OutboundHttp
19
+ readonly limits?: ImageLimits
13
20
  /** Host stores kind=generated under context.actor and returns a controlled display URL. */
14
21
  readonly persist?: (image: ImagePart, context: WorkerContext) => Promise<ImageRef>
15
22
  }
@@ -21,26 +28,46 @@ export class ImageService {
21
28
  this.options = options
22
29
  }
23
30
 
24
- async generate(connection: ImageConnection, input: ImageRequest, context: WorkerContext): Promise<GeneratedImage> {
25
- const image = await generateImage(this.options.outbound, connection, input, context.signal)
31
+ async generate(
32
+ connection: ImageConnection,
33
+ input: ImageRequest,
34
+ context: WorkerContext,
35
+ ): Promise<GeneratedImage> {
36
+ const result = await generateImage(
37
+ this.options.outbound,
38
+ connection,
39
+ input,
40
+ context.signal,
41
+ this.options.limits,
42
+ )
43
+ const { image } = result
26
44
  if (!this.options.persist) {
27
45
  return {
28
- image,
46
+ ...result,
29
47
  reference: { mime: image.mime, url: `data:${image.mime};base64,${image.data}` },
30
48
  persisted: false,
31
49
  }
32
50
  }
33
51
  try {
34
52
  if (context.signal.aborted)
35
- throw new ImageFailure('cancelled', 'Image generated; persistence was cancelled', 'generated')
53
+ throw new ImageFailure(
54
+ 'cancelled',
55
+ 'Image generated; persistence was cancelled',
56
+ 'generated',
57
+ )
36
58
  const reference = await this.options.persist(image, context)
37
- return { image, reference, persisted: true }
59
+ return { ...result, reference, persisted: true }
38
60
  } catch {
39
61
  // Generation succeeded. Do not claim the provider did nothing or that saving succeeded.
40
- throw new ImageFailure('internal', 'Image generated but persistence could not be confirmed', 'generated', {
41
- field: 'images',
42
- reason: 'generated_save_failed',
43
- })
62
+ throw new ImageFailure(
63
+ 'internal',
64
+ 'Image generated but persistence could not be confirmed',
65
+ 'generated',
66
+ {
67
+ field: 'images',
68
+ reason: 'generated_save_failed',
69
+ },
70
+ )
44
71
  }
45
72
  }
46
73
  }
package/src/logger.ts CHANGED
@@ -58,18 +58,29 @@ export function createLogger(options: {
58
58
  }): Logger {
59
59
  const routes = new Set(options.routes)
60
60
  for (const route of routes)
61
- if (!/^\/[A-Za-z0-9_/:.{}-]*$/u.test(route) || route.length > 256) throw new Error('Invalid log route template')
61
+ if (!/^\/[A-Za-z0-9_/:.{}-]*$/u.test(route) || route.length > 256)
62
+ throw new Error('Invalid log route template')
62
63
  return Object.freeze({
63
64
  log(event: LogEvent, fields: Readonly<Record<string, unknown>>): void {
64
65
  if (!Object.hasOwn(EVENTS, event)) throw new Error('Invalid log event')
65
66
  const safe: Record<string, LogValue> = {}
66
67
  for (const [key, value] of Object.entries(fields)) {
67
- if (Object.hasOwn(IDENTIFIERS, key) && typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,128}$/u.test(value))
68
+ if (
69
+ Object.hasOwn(IDENTIFIERS, key) &&
70
+ typeof value === 'string' &&
71
+ /^[A-Za-z0-9_.:-]{1,128}$/u.test(value)
72
+ )
68
73
  safe[key] = value
69
- else if (Object.hasOwn(COUNTERS, key) && typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
74
+ else if (
75
+ Object.hasOwn(COUNTERS, key) &&
76
+ typeof value === 'number' &&
77
+ Number.isSafeInteger(value) &&
78
+ value >= 0
79
+ )
70
80
  safe[key] = value
71
81
  else if (key === 'usageAvailable' && typeof value === 'boolean') safe[key] = value
72
- else if (key === 'route' && typeof value === 'string' && routes.has(value)) safe[key] = value
82
+ else if (key === 'route' && typeof value === 'string' && routes.has(value))
83
+ safe[key] = value
73
84
  else if (
74
85
  key === 'state' &&
75
86
  (value === 'accepted' ||
package/src/mcp/auth.ts CHANGED
@@ -2,7 +2,18 @@ import { KitFailure, badRequest } from '@ouraihub/aether-core/error'
2
2
  import type { Actor } from '../context.js'
3
3
  import type { StoredDocument } from '../storage.js'
4
4
  import type { ApiKeyRecord, GrantRecord, McpServiceOptions, McpSettings } from './types.js'
5
- import { equal, forbidden, hash, notFound, object, random, strings, text, unique, validActor } from './common.js'
5
+ import {
6
+ equal,
7
+ forbidden,
8
+ hash,
9
+ notFound,
10
+ object,
11
+ random,
12
+ strings,
13
+ text,
14
+ unique,
15
+ validActor,
16
+ } from './common.js'
6
17
 
7
18
  interface KeyState {
8
19
  record: ApiKeyRecord
@@ -106,11 +117,14 @@ export class McpAuth {
106
117
  forbidden()
107
118
  }
108
119
  requested(value: unknown, actor?: Actor): string[] {
109
- if (!strings(value) || value.length === 0 || value.length > 100) throw badRequest('scopes', 'invalid')
120
+ if (!strings(value) || value.length === 0 || value.length > 100)
121
+ throw badRequest('scopes', 'invalid')
110
122
  const scopes = unique(value)
111
123
  if (
112
124
  scopes.some(
113
- (scope) => !this.options.scopes.some((known) => known.id === scope) || (actor && !actor.scopes.includes(scope)),
125
+ (scope) =>
126
+ !this.options.scopes.some((known) => known.id === scope) ||
127
+ (actor && !actor.scopes.includes(scope)),
114
128
  )
115
129
  )
116
130
  forbidden()
@@ -119,17 +133,30 @@ export class McpAuth {
119
133
  async settings(owner: Actor): Promise<McpSettings> {
120
134
  const doc = await this.options.store.get(owner, 'mcp-settings', 'settings')
121
135
  if (!doc) return { enabled: false, enabledScopes: [] }
122
- if (!object(doc.value) || typeof doc.value.enabled !== 'boolean' || !strings(doc.value.enabledScopes))
136
+ if (
137
+ !object(doc.value) ||
138
+ typeof doc.value.enabled !== 'boolean' ||
139
+ !strings(doc.value.enabledScopes)
140
+ )
123
141
  throw new KitFailure('internal', 'Invalid MCP settings')
124
142
  return { enabled: doc.value.enabled, enabledScopes: doc.value.enabledScopes }
125
143
  }
126
144
  async saveSettings(actor: Actor, value: Record<string, unknown>): Promise<McpSettings> {
127
145
  this.actor(actor)
128
- if (typeof value.enabled !== 'boolean' || !strings(value.enabledScopes)) throw badRequest('settings', 'invalid')
129
- const enabledScopes = value.enabledScopes.length ? this.requested(value.enabledScopes, actor) : []
146
+ if (typeof value.enabled !== 'boolean' || !strings(value.enabledScopes))
147
+ throw badRequest('settings', 'invalid')
148
+ const enabledScopes = value.enabledScopes.length
149
+ ? this.requested(value.enabledScopes, actor)
150
+ : []
130
151
  const current = await this.options.store.get(actor, 'mcp-settings', 'settings')
131
152
  const settings = { enabled: value.enabled, enabledScopes }
132
- await this.options.store.compareAndSet(actor, 'mcp-settings', 'settings', current?.revision ?? null, settings)
153
+ await this.options.store.compareAndSet(
154
+ actor,
155
+ 'mcp-settings',
156
+ 'settings',
157
+ current?.revision ?? null,
158
+ settings,
159
+ )
133
160
  return settings
134
161
  }
135
162
  async keys(actor: Actor): Promise<ApiKeyRecord[]> {
@@ -144,7 +171,8 @@ export class McpAuth {
144
171
  result.push(...page.map((doc) => readKey(doc).record))
145
172
  if (page.length < 200) return result
146
173
  const next = page[page.length - 1]?.key
147
- if (!next || (after !== undefined && next <= after)) throw new KitFailure('internal', 'Invalid key pagination')
174
+ if (!next || (after !== undefined && next <= after))
175
+ throw new KitFailure('internal', 'Invalid key pagination')
148
176
  after = next
149
177
  }
150
178
  }
@@ -160,7 +188,8 @@ export class McpAuth {
160
188
  result.push(...page.map((doc) => readGrant(doc).record))
161
189
  if (page.length < 200) return result
162
190
  const next = page[page.length - 1]?.key
163
- if (!next || (after !== undefined && next <= after)) throw new KitFailure('internal', 'Invalid grant pagination')
191
+ if (!next || (after !== undefined && next <= after))
192
+ throw new KitFailure('internal', 'Invalid grant pagination')
164
193
  after = next
165
194
  }
166
195
  }
@@ -175,7 +204,10 @@ export class McpAuth {
175
204
  if (!(await this.options.authorizeConsent(actor, scopes, request))) forbidden()
176
205
  const now = this.now()
177
206
  const expiresAt = input.expiresAt === undefined ? now + 90 * 86400000 : input.expiresAt
178
- if (expiresAt !== null && (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt) || expiresAt <= now))
207
+ if (
208
+ expiresAt !== null &&
209
+ (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt) || expiresAt <= now)
210
+ )
179
211
  throw badRequest('expiresAt', 'invalid')
180
212
  const id = crypto.randomUUID()
181
213
  const token = `aether_key_${random()}`
@@ -249,7 +281,9 @@ export class McpAuth {
249
281
  const settings = await this.settings(owner)
250
282
  if (!settings.enabled) return null
251
283
  const effective = scopes.filter(
252
- (scope) => settings.enabledScopes.includes(scope) && this.options.scopes.some((known) => known.id === scope),
284
+ (scope) =>
285
+ settings.enabledScopes.includes(scope) &&
286
+ this.options.scopes.some((known) => known.id === scope),
253
287
  )
254
288
  return {
255
289
  actor: { ...owner, kind: 'mcp_client', sessionId: null, scopes: effective },
@@ -302,7 +336,10 @@ export class McpAuth {
302
336
  const document = await this.options.store.get(owner, 'mcp-grants', grantId)
303
337
  if (!document) throw new KitFailure('unauthorized', 'Invalid grant')
304
338
  const state = readGrant(document)
305
- if (!this.activeGrant(state, owner) || (clientId !== undefined && state.record.clientId !== clientId))
339
+ if (
340
+ !this.activeGrant(state, owner) ||
341
+ (clientId !== undefined && state.record.clientId !== clientId)
342
+ )
306
343
  throw new KitFailure('unauthorized', 'Invalid grant')
307
344
  if (
308
345
  refresh !== undefined &&
@@ -319,13 +356,24 @@ export class McpAuth {
319
356
  generation: state.generation + 1,
320
357
  }
321
358
  await this.options.store.compareAndSet(owner, 'mcp-grants', grantId, document.revision, updated)
322
- await this.index(await hash(accessToken), { owner, id: grantId, kind: 'access', expiresAt: now + 3600000 })
323
- await this.options.store.compareAndSet(this.system, 'mcp-refresh-index', await hash(refreshToken), null, {
359
+ await this.index(await hash(accessToken), {
324
360
  owner,
325
- grantId,
326
- clientId: state.record.clientId,
327
- audience: `${this.options.origin}/mcp`,
361
+ id: grantId,
362
+ kind: 'access',
363
+ expiresAt: now + 3600000,
328
364
  })
365
+ await this.options.store.compareAndSet(
366
+ this.system,
367
+ 'mcp-refresh-index',
368
+ await hash(refreshToken),
369
+ null,
370
+ {
371
+ owner,
372
+ grantId,
373
+ clientId: state.record.clientId,
374
+ audience: `${this.options.origin}/mcp`,
375
+ },
376
+ )
329
377
  return {
330
378
  access_token: accessToken,
331
379
  token_type: 'Bearer',
package/src/mcp/client.ts CHANGED
@@ -10,7 +10,12 @@ import {
10
10
  } from '@ouraihub/aether-core/mcp'
11
11
  import type { OutboundHttp } from '@ouraihub/aether-core/ports'
12
12
  import { ownerKey, type Actor, type WorkerContext } from '../context.js'
13
- import { isSealedCredential, type CredentialPort, type CredentialScope, type SealedCredential } from '../credentials.js'
13
+ import {
14
+ isSealedCredential,
15
+ type CredentialPort,
16
+ type CredentialScope,
17
+ type SealedCredential,
18
+ } from '../credentials.js'
14
19
  import type { DocumentStore } from '../storage.js'
15
20
 
16
21
  const COLLECTION = 'mcp-client'
@@ -70,7 +75,8 @@ function bool(value: unknown, field: string, fallback?: boolean): boolean {
70
75
  return value
71
76
  }
72
77
  function approval(value: unknown): Approval {
73
- if (value !== 'auto' && value !== 'ask' && value !== 'deny') throw badRequest('approval', 'invalid')
78
+ if (value !== 'auto' && value !== 'ask' && value !== 'deny')
79
+ throw badRequest('approval', 'invalid')
74
80
  return value
75
81
  }
76
82
  function looksSecret(value: string): boolean {
@@ -95,7 +101,8 @@ function serverUrl(raw: unknown, allowPrivate: boolean): string {
95
101
  throw badRequest('url', 'credential_query')
96
102
  }
97
103
  const host = url.hostname.replace(/^\[|\]$/g, '')
98
- const loopback = host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127\./.test(host)
104
+ const loopback =
105
+ host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127\./.test(host)
99
106
  if (url.protocol === 'http:' && !allowPrivate) throw badRequest('url', 'plain_http')
100
107
  // DNS and connection-time restrictions remain the injected outbound port's responsibility.
101
108
  const restricted =
@@ -112,7 +119,8 @@ function maskedUrl(raw: string): string {
112
119
  url.username = ''
113
120
  url.password = ''
114
121
  for (const [name, value] of url.searchParams)
115
- if (Object.hasOwn(SECRET_PARAMS, name.toLowerCase()) || looksSecret(value)) url.searchParams.set(name, '***')
122
+ if (Object.hasOwn(SECRET_PARAMS, name.toLowerCase()) || looksSecret(value))
123
+ url.searchParams.set(name, '***')
116
124
  return url.href
117
125
  }
118
126
  function strings(value: unknown, field: string): Record<string, string> {
@@ -121,7 +129,12 @@ function strings(value: unknown, field: string): Record<string, string> {
121
129
  const result: Record<string, string> = {}
122
130
  for (const [key, entry] of Object.entries(value)) {
123
131
  if (!key || typeof entry !== 'string') throw badRequest(field, 'invalid')
124
- Object.defineProperty(result, key, { value: entry, enumerable: true, writable: true, configurable: true })
132
+ Object.defineProperty(result, key, {
133
+ value: entry,
134
+ enumerable: true,
135
+ writable: true,
136
+ configurable: true,
137
+ })
125
138
  }
126
139
  return result
127
140
  }
@@ -150,7 +163,8 @@ function cached(value: unknown): McpCachedTool {
150
163
  )
151
164
  throw badRequest('tools', 'invalid')
152
165
  if ([...value.description].length > 1024) throw badRequest('tools', 'too_large')
153
- if (new TextEncoder().encode(JSON.stringify(value.schema)).length > 32768) throw badRequest('tools', 'too_large')
166
+ if (new TextEncoder().encode(JSON.stringify(value.schema)).length > 32768)
167
+ throw badRequest('tools', 'too_large')
154
168
  return {
155
169
  name: text(value.name, 'tool'),
156
170
  description: value.description,
@@ -163,12 +177,18 @@ function cached(value: unknown): McpCachedTool {
163
177
  function toolList(value: unknown): McpCachedTool[] {
164
178
  if (!Array.isArray(value) || value.length > 200) throw badRequest('tools', 'invalid')
165
179
  const tools = value.map(cached)
166
- if (new Set(tools.map((tool) => tool.name)).size !== tools.length) throw badRequest('tools', 'duplicate')
180
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length)
181
+ throw badRequest('tools', 'duplicate')
167
182
  return tools
168
183
  }
169
184
  function readState(value: unknown): State {
170
185
  try {
171
- if (!object(value) || value.version !== 1 || !Array.isArray(value.servers) || !Array.isArray(value.deleted))
186
+ if (
187
+ !object(value) ||
188
+ value.version !== 1 ||
189
+ !Array.isArray(value.servers) ||
190
+ !Array.isArray(value.deleted)
191
+ )
172
192
  throw badRequest('mcp', 'invalid')
173
193
  fields(value, ['version', 'servers', 'deleted'], 'mcp')
174
194
  const deleted = value.deleted.map((id: unknown) => text(id, 'id'))
@@ -176,7 +196,18 @@ function readState(value: unknown): State {
176
196
  if (!object(entry)) throw badRequest('mcp', 'invalid')
177
197
  fields(
178
198
  entry,
179
- ['id', 'slug', 'name', 'url', 'enabled', 'trusted', 'allowPrivate', 'secret', 'secretKeys', 'tools'],
199
+ [
200
+ 'id',
201
+ 'slug',
202
+ 'name',
203
+ 'url',
204
+ 'enabled',
205
+ 'trusted',
206
+ 'allowPrivate',
207
+ 'secret',
208
+ 'secretKeys',
209
+ 'tools',
210
+ ],
180
211
  'mcp',
181
212
  )
182
213
  const slug = text(entry.slug, 'slug')
@@ -186,7 +217,8 @@ function readState(value: unknown): State {
186
217
  !(entry.secret === null || isSealedCredential(entry.secret))
187
218
  )
188
219
  throw badRequest('mcp', 'invalid')
189
- if (entry.secret !== null) fields(entry.secret, ['version', 'keyId', 'nonce', 'ciphertext'], 'secret')
220
+ if (entry.secret !== null)
221
+ fields(entry.secret, ['version', 'keyId', 'nonce', 'ciphertext'], 'secret')
190
222
  if (
191
223
  new Set(entry.secretKeys).size !== entry.secretKeys.length ||
192
224
  (entry.secret !== null && !entry.secretKeys.length)
@@ -211,7 +243,8 @@ function readState(value: unknown): State {
211
243
  new Set(servers.map((server) => server.slug)).size !== servers.length ||
212
244
  new Set(deleted).size !== deleted.length ||
213
245
  servers.some(
214
- (server) => deleted.includes(server.id) || (server.secret === null && server.secretKeys.length !== 0),
246
+ (server) =>
247
+ deleted.includes(server.id) || (server.secret === null && server.secretKeys.length !== 0),
215
248
  )
216
249
  )
217
250
  throw badRequest('mcp', 'invalid')
@@ -221,7 +254,12 @@ function readState(value: unknown): State {
221
254
  }
222
255
  }
223
256
  function toolDto(tool: McpCachedTool): ToolDto {
224
- return { name: tool.name, description: tool.description, readOnly: tool.readOnly, approval: tool.approval }
257
+ return {
258
+ name: tool.name,
259
+ description: tool.description,
260
+ readOnly: tool.readOnly,
261
+ approval: tool.approval,
262
+ }
225
263
  }
226
264
  function dto(server: Server): ServerDto {
227
265
  return {
@@ -288,7 +326,9 @@ export class McpClientSettings {
288
326
  const value: unknown = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
289
327
  secret = parseSecret(value)
290
328
  } catch {
291
- throw new KitFailure('needs_config', 'MCP credentials are unreadable', { reason: 'key_unreadable' })
329
+ throw new KitFailure('needs_config', 'MCP credentials are unreadable', {
330
+ reason: 'key_unreadable',
331
+ })
292
332
  } finally {
293
333
  bytes?.fill(0)
294
334
  }
@@ -326,7 +366,10 @@ export class McpClientSettings {
326
366
  const server = snapshot.state.servers.find((entry) => entry.id === id)
327
367
  if (!server) throw missing()
328
368
  const approvals = new Map(server.tools.map((tool) => [tool.name, tool.approval]))
329
- server.tools = toolList(tools).map((tool) => ({ ...tool, approval: approvals.get(tool.name) ?? 'ask' }))
369
+ server.tools = toolList(tools).map((tool) => ({
370
+ ...tool,
371
+ approval: approvals.get(tool.name) ?? 'ask',
372
+ }))
330
373
  await this.commit(owner, snapshot)
331
374
  },
332
375
  }
@@ -334,18 +377,31 @@ export class McpClientSettings {
334
377
  this.runtimes.set(key, runtime)
335
378
  return runtime
336
379
  }
337
- private async save(actor: Actor, input: Record<string, unknown>, id?: string): Promise<ServerDto[]> {
338
- fields(input, ['id', 'slug', 'name', 'transport', 'secret', 'enabled', 'trusted', 'allowPrivate'], 'body')
380
+ private async save(
381
+ actor: Actor,
382
+ input: Record<string, unknown>,
383
+ id?: string,
384
+ ): Promise<ServerDto[]> {
385
+ fields(
386
+ input,
387
+ ['id', 'slug', 'name', 'transport', 'secret', 'enabled', 'trusted', 'allowPrivate'],
388
+ 'body',
389
+ )
339
390
  if (input.id !== undefined && input.id !== id) throw badRequest('id', 'mismatch')
340
391
  const snapshot = await this.snapshot(actor)
341
- const existing = id === undefined ? undefined : snapshot.state.servers.find((server) => server.id === id)
392
+ const existing =
393
+ id === undefined ? undefined : snapshot.state.servers.find((server) => server.id === id)
342
394
  if (id !== undefined && !existing) throw missing()
343
395
  const slug = text(input.slug, 'slug')
344
396
  if (!/^[a-z0-9_]{1,24}$/.test(slug)) throw badRequest('slug', 'invalid')
345
397
  if (snapshot.state.servers.some((server) => server.slug === slug && server.id !== id))
346
- throw new KitFailure('conflict', 'MCP slug already exists', { reason: 'duplicate_slug', field: 'slug' })
398
+ throw new KitFailure('conflict', 'MCP slug already exists', {
399
+ reason: 'duplicate_slug',
400
+ field: 'slug',
401
+ })
347
402
  if (!object(input.transport)) throw badRequest('transport', 'invalid')
348
- if (input.transport.kind === 'stdio') throw new KitFailure('unsupported', 'Worker MCP clients support HTTP only')
403
+ if (input.transport.kind === 'stdio')
404
+ throw new KitFailure('unsupported', 'Worker MCP clients support HTTP only')
349
405
  fields(input.transport, ['kind', 'url'], 'transport')
350
406
  if (input.transport.kind !== 'http') throw badRequest('transport', 'invalid')
351
407
  const allowPrivate = bool(input.allowPrivate, 'allowPrivate', false)
@@ -363,7 +419,9 @@ export class McpClientSettings {
363
419
  }
364
420
  if (input.secret !== undefined) {
365
421
  const secret = parseSecret(input.secret)
366
- server.secretKeys = [...new Set([...Object.keys(secret.headers), ...Object.keys(secret.query)])].sort()
422
+ server.secretKeys = [
423
+ ...new Set([...Object.keys(secret.headers), ...Object.keys(secret.query)]),
424
+ ].sort()
367
425
  if (server.secretKeys.length) {
368
426
  const bytes = new TextEncoder().encode(JSON.stringify(secret))
369
427
  try {
@@ -380,7 +438,10 @@ export class McpClientSettings {
380
438
  }
381
439
  async handle(request: Request, context: WorkerContext): Promise<Response | null> {
382
440
  const path = new URL(request.url).pathname
383
- const match = /^\/api\/v1\/mcp\/servers(?:\/([^/]+)(?:\/(refresh)|\/tools\/([^/]+)\/(approval))?)?$/.exec(path)
441
+ const match =
442
+ /^\/api\/v1\/mcp\/servers(?:\/([^/]+)(?:\/(refresh)|\/tools\/([^/]+)\/(approval))?)?$/.exec(
443
+ path,
444
+ )
384
445
  if (!match) return null
385
446
  let id: string | undefined
386
447
  let tool: string | undefined
@@ -391,8 +452,10 @@ export class McpClientSettings {
391
452
  throw badRequest('path', 'invalid')
392
453
  }
393
454
  const actor = context.actor
394
- if (!id && request.method === 'GET') return Response.json((await this.snapshot(actor)).state.servers.map(dto))
395
- if (!id && request.method === 'POST') return Response.json(await this.save(actor, await body(request)))
455
+ if (!id && request.method === 'GET')
456
+ return Response.json((await this.snapshot(actor)).state.servers.map(dto))
457
+ if (!id && request.method === 'POST')
458
+ return Response.json(await this.save(actor, await body(request)))
396
459
  if (id && !match[2] && !match[4] && request.method === 'PUT')
397
460
  return Response.json(await this.save(actor, await body(request), id))
398
461
  if (id && !match[2] && !match[4] && request.method === 'DELETE') {
@@ -417,7 +480,9 @@ export class McpClientSettings {
417
480
  const server = snapshot.state.servers.find((entry) => entry.id === id)
418
481
  const cached = server?.tools.find((entry) => entry.name === tool)
419
482
  if (!server || !cached) throw missing()
420
- server.tools = server.tools.map((entry) => (entry === cached ? { ...entry, approval: value } : entry))
483
+ server.tools = server.tools.map((entry) =>
484
+ entry === cached ? { ...entry, approval: value } : entry,
485
+ )
421
486
  await this.commit(actor, snapshot)
422
487
  return Response.json({ ok: true })
423
488
  }
package/src/mcp/common.ts CHANGED
@@ -9,7 +9,8 @@ export function strings(value: unknown): value is string[] {
9
9
  return Array.isArray(value) && value.every((item: unknown) => typeof item === 'string')
10
10
  }
11
11
  export function text(value: unknown, field: string, max = 1024): string {
12
- if (typeof value !== 'string' || !value.trim() || value.length > max) throw badRequest(field, 'invalid')
12
+ if (typeof value !== 'string' || !value.trim() || value.length > max)
13
+ throw badRequest(field, 'invalid')
13
14
  return value
14
15
  }
15
16
  export function unique(values: readonly string[]): string[] {
@@ -24,7 +25,9 @@ export function base64url(bytes: Uint8Array): string {
24
25
  return btoa(value).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '')
25
26
  }
26
27
  export async function hash(value: string): Promise<string> {
27
- return base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))))
28
+ return base64url(
29
+ new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))),
30
+ )
28
31
  }
29
32
  export function equal(a: string, b: string): boolean {
30
33
  let difference = a.length ^ b.length
@@ -33,7 +36,8 @@ export function equal(a: string, b: string): boolean {
33
36
  return difference === 0
34
37
  }
35
38
  export async function body(request: Request, max = 65536): Promise<string> {
36
- if (Number(request.headers.get('Content-Length')) > max) throw badRequest('body', 'too_large', max)
39
+ if (Number(request.headers.get('Content-Length')) > max)
40
+ throw badRequest('body', 'too_large', max)
37
41
  if (!request.body) return ''
38
42
  const reader = request.body.getReader()
39
43
  const chunks: Uint8Array[] = []