@orchid-labs/pluxx 0.1.0 → 0.1.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 +100 -522
- package/dist/cli/agent.d.ts +7 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/index.d.ts +19 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-from-mcp.d.ts +17 -2
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +5 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts +10 -0
- package/dist/cli/mcp-proxy.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/test.d.ts +2 -0
- package/dist/cli/test.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts +2 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +1 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -1
- package/dist/mcp/introspect.d.ts +43 -1
- package/dist/mcp/introspect.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/validation/platform-rules.d.ts +20 -0
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/cli/agent.ts +459 -34
- package/src/cli/doctor.ts +400 -1
- package/src/cli/eval.ts +470 -0
- package/src/cli/index.ts +633 -114
- package/src/cli/init-from-mcp.ts +545 -41
- package/src/cli/install.ts +166 -4
- package/src/cli/lint.ts +56 -26
- package/src/cli/mcp-proxy.ts +322 -0
- package/src/cli/migrate.ts +256 -3
- package/src/cli/sync-from-mcp.ts +23 -0
- package/src/cli/test.ts +10 -2
- package/src/generators/claude-code/index.ts +143 -0
- package/src/generators/codex/index.ts +23 -0
- package/src/index.ts +12 -1
- package/src/mcp/introspect.ts +297 -24
- package/src/permissions.ts +3 -1
- package/src/validation/platform-rules.ts +121 -0
package/src/mcp/introspect.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { McpAuth, McpServer } from '../schema'
|
|
|
5
5
|
const MCP_PROTOCOL_VERSION = '2025-03-26'
|
|
6
6
|
const CLIENT_INFO = {
|
|
7
7
|
name: 'pluxx',
|
|
8
|
-
version: '0.1.
|
|
8
|
+
version: '0.1.1',
|
|
9
9
|
}
|
|
10
10
|
const DEFAULT_TIMEOUT_MS = 10_000
|
|
11
11
|
|
|
@@ -16,6 +16,33 @@ export interface IntrospectedMcpTool {
|
|
|
16
16
|
inputSchema?: Record<string, unknown>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface IntrospectedMcpResource {
|
|
20
|
+
uri: string
|
|
21
|
+
name?: string
|
|
22
|
+
title?: string
|
|
23
|
+
description?: string
|
|
24
|
+
mimeType?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IntrospectedMcpResourceTemplate {
|
|
28
|
+
uriTemplate: string
|
|
29
|
+
name: string
|
|
30
|
+
title?: string
|
|
31
|
+
description?: string
|
|
32
|
+
mimeType?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IntrospectedMcpPrompt {
|
|
36
|
+
name: string
|
|
37
|
+
title?: string
|
|
38
|
+
description?: string
|
|
39
|
+
arguments?: Array<{
|
|
40
|
+
name: string
|
|
41
|
+
description?: string
|
|
42
|
+
required?: boolean
|
|
43
|
+
}>
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
export interface IntrospectedMcpServer {
|
|
20
47
|
protocolVersion: string
|
|
21
48
|
instructions?: string
|
|
@@ -27,6 +54,17 @@ export interface IntrospectedMcpServer {
|
|
|
27
54
|
websiteUrl?: string
|
|
28
55
|
}
|
|
29
56
|
tools: IntrospectedMcpTool[]
|
|
57
|
+
resources?: IntrospectedMcpResource[]
|
|
58
|
+
resourceTemplates?: IntrospectedMcpResourceTemplate[]
|
|
59
|
+
prompts?: IntrospectedMcpPrompt[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface McpAuthDiscovery {
|
|
63
|
+
kind: 'bearer' | 'header' | 'platform'
|
|
64
|
+
mode?: 'oauth'
|
|
65
|
+
headerName?: string
|
|
66
|
+
authorizationUrl?: string
|
|
67
|
+
resourceMetadataUrl?: string
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
export class McpIntrospectionError extends Error {
|
|
@@ -38,12 +76,52 @@ export class McpIntrospectionError extends Error {
|
|
|
38
76
|
responseBodySnippet?: string
|
|
39
77
|
responseUrl?: string
|
|
40
78
|
},
|
|
79
|
+
readonly rpcCode?: number,
|
|
41
80
|
) {
|
|
42
81
|
super(message)
|
|
43
82
|
this.name = 'McpIntrospectionError'
|
|
44
83
|
}
|
|
45
84
|
}
|
|
46
85
|
|
|
86
|
+
export function discoverMcpAuthFromError(error: McpIntrospectionError): McpAuthDiscovery | null {
|
|
87
|
+
const wwwAuthenticate = error.context?.responseHeaders?.['www-authenticate']?.trim() ?? ''
|
|
88
|
+
const locationHeader = error.context?.responseHeaders?.location?.trim()
|
|
89
|
+
const responseUrl = error.context?.responseUrl?.trim()
|
|
90
|
+
const responseBody = error.context?.responseBodySnippet ?? ''
|
|
91
|
+
const message = error.message
|
|
92
|
+
|
|
93
|
+
const authorizationUri = extractAuthParam(wwwAuthenticate, 'authorization_uri')
|
|
94
|
+
const resourceMetadataUrl = extractAuthParam(wwwAuthenticate, 'resource_metadata')
|
|
95
|
+
const oauthUrl = authorizationUri
|
|
96
|
+
?? pickOAuthUrl(locationHeader, responseUrl)
|
|
97
|
+
|
|
98
|
+
if (looksLikeOAuthSignal([wwwAuthenticate, locationHeader, responseUrl, responseBody, message])) {
|
|
99
|
+
return {
|
|
100
|
+
kind: 'platform',
|
|
101
|
+
mode: 'oauth',
|
|
102
|
+
...(oauthUrl ? { authorizationUrl: oauthUrl } : {}),
|
|
103
|
+
...(resourceMetadataUrl ? { resourceMetadataUrl } : {}),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const headerName = extractHeaderName([wwwAuthenticate, responseBody, message])
|
|
108
|
+
if (headerName && headerName.toLowerCase() !== 'authorization') {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'header',
|
|
111
|
+
headerName,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (wwwAuthenticate.toLowerCase().includes('bearer') || headerName === 'Authorization') {
|
|
116
|
+
return {
|
|
117
|
+
kind: 'bearer',
|
|
118
|
+
headerName: 'Authorization',
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
47
125
|
interface JsonRpcSuccess<T> {
|
|
48
126
|
jsonrpc: '2.0'
|
|
49
127
|
id: number | string
|
|
@@ -65,6 +143,11 @@ type JsonRpcEnvelope<T> = JsonRpcSuccess<T> | JsonRpcFailure
|
|
|
65
143
|
interface InitializeResult {
|
|
66
144
|
protocolVersion?: string
|
|
67
145
|
instructions?: string
|
|
146
|
+
capabilities?: {
|
|
147
|
+
tools?: Record<string, unknown>
|
|
148
|
+
resources?: Record<string, unknown>
|
|
149
|
+
prompts?: Record<string, unknown>
|
|
150
|
+
}
|
|
68
151
|
serverInfo?: {
|
|
69
152
|
name?: string
|
|
70
153
|
title?: string
|
|
@@ -84,39 +167,80 @@ interface ListToolsResult {
|
|
|
84
167
|
nextCursor?: string
|
|
85
168
|
}
|
|
86
169
|
|
|
87
|
-
interface
|
|
170
|
+
interface ListResourcesResult {
|
|
171
|
+
resources?: Array<{
|
|
172
|
+
uri: string
|
|
173
|
+
name?: string
|
|
174
|
+
title?: string
|
|
175
|
+
description?: string
|
|
176
|
+
mimeType?: string
|
|
177
|
+
}>
|
|
178
|
+
nextCursor?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface ListResourceTemplatesResult {
|
|
182
|
+
resourceTemplates?: Array<{
|
|
183
|
+
uriTemplate: string
|
|
184
|
+
name: string
|
|
185
|
+
title?: string
|
|
186
|
+
description?: string
|
|
187
|
+
mimeType?: string
|
|
188
|
+
}>
|
|
189
|
+
nextCursor?: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface ListPromptsResult {
|
|
193
|
+
prompts?: Array<{
|
|
194
|
+
name: string
|
|
195
|
+
title?: string
|
|
196
|
+
description?: string
|
|
197
|
+
arguments?: Array<{
|
|
198
|
+
name: string
|
|
199
|
+
description?: string
|
|
200
|
+
required?: boolean
|
|
201
|
+
}>
|
|
202
|
+
}>
|
|
203
|
+
nextCursor?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface McpClient {
|
|
88
207
|
request<T>(method: string, params?: Record<string, unknown>): Promise<T>
|
|
89
208
|
notify(method: string, params?: Record<string, unknown>): Promise<void>
|
|
90
209
|
close(): Promise<void>
|
|
91
210
|
}
|
|
92
211
|
|
|
93
212
|
export async function introspectMcpServer(server: McpServer): Promise<IntrospectedMcpServer> {
|
|
213
|
+
if (server.transport === 'http') {
|
|
214
|
+
try {
|
|
215
|
+
return await introspectWithClient(await createMcpClient(server))
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (
|
|
218
|
+
error instanceof McpIntrospectionError
|
|
219
|
+
&& (error.status === 400 || error.status === 404 || error.status === 405)
|
|
220
|
+
) {
|
|
221
|
+
return await introspectWithClient(await createSseClient({
|
|
222
|
+
...server,
|
|
223
|
+
transport: 'sse',
|
|
224
|
+
}))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return await introspectWithClient(await createMcpClient(server))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function createMcpClient(server: McpServer): Promise<McpClient> {
|
|
94
235
|
if (server.transport === 'stdio') {
|
|
95
|
-
|
|
96
|
-
return await introspectWithClient(client)
|
|
236
|
+
return await createStdioClient(server)
|
|
97
237
|
}
|
|
98
238
|
|
|
99
239
|
if (server.transport === 'sse') {
|
|
100
|
-
|
|
101
|
-
return await introspectWithClient(client)
|
|
240
|
+
return await createSseClient(server)
|
|
102
241
|
}
|
|
103
242
|
|
|
104
|
-
|
|
105
|
-
return await introspectWithClient(createHttpClient(server))
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (
|
|
108
|
-
error instanceof McpIntrospectionError
|
|
109
|
-
&& (error.status === 400 || error.status === 404 || error.status === 405)
|
|
110
|
-
) {
|
|
111
|
-
const sseClient = await createSseClient({
|
|
112
|
-
...server,
|
|
113
|
-
transport: 'sse',
|
|
114
|
-
})
|
|
115
|
-
return await introspectWithClient(sseClient)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw error
|
|
119
|
-
}
|
|
243
|
+
return createHttpClient(server)
|
|
120
244
|
}
|
|
121
245
|
|
|
122
246
|
async function introspectWithClient(client: McpClient): Promise<IntrospectedMcpServer> {
|
|
@@ -135,6 +259,15 @@ async function introspectWithClient(client: McpClient): Promise<IntrospectedMcpS
|
|
|
135
259
|
'The MCP server initialized successfully but exposed no tools. pluxx init --from-mcp currently scaffolds from tool metadata only.',
|
|
136
260
|
)
|
|
137
261
|
}
|
|
262
|
+
const resources = hasInitializeCapability(initialize, 'resources')
|
|
263
|
+
? await listAllResources(client)
|
|
264
|
+
: []
|
|
265
|
+
const resourceTemplates = hasInitializeCapability(initialize, 'resources')
|
|
266
|
+
? await listAllResourceTemplates(client)
|
|
267
|
+
: []
|
|
268
|
+
const prompts = hasInitializeCapability(initialize, 'prompts')
|
|
269
|
+
? await listAllPrompts(client)
|
|
270
|
+
: []
|
|
138
271
|
|
|
139
272
|
return {
|
|
140
273
|
protocolVersion: initialize.protocolVersion ?? MCP_PROTOCOL_VERSION,
|
|
@@ -147,12 +280,79 @@ async function introspectWithClient(client: McpClient): Promise<IntrospectedMcpS
|
|
|
147
280
|
websiteUrl: initialize.serverInfo?.websiteUrl,
|
|
148
281
|
},
|
|
149
282
|
tools,
|
|
283
|
+
resources,
|
|
284
|
+
resourceTemplates,
|
|
285
|
+
prompts,
|
|
150
286
|
}
|
|
151
287
|
} finally {
|
|
152
288
|
await client.close()
|
|
153
289
|
}
|
|
154
290
|
}
|
|
155
291
|
|
|
292
|
+
function hasInitializeCapability(
|
|
293
|
+
initialize: InitializeResult,
|
|
294
|
+
capability: 'resources' | 'prompts',
|
|
295
|
+
): boolean {
|
|
296
|
+
const value = initialize.capabilities?.[capability]
|
|
297
|
+
return typeof value === 'object' && value !== null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function looksLikeOAuthSignal(values: Array<string | undefined>): boolean {
|
|
301
|
+
return values.some((value) => {
|
|
302
|
+
const normalized = value?.toLowerCase() ?? ''
|
|
303
|
+
return /\boauth\b/.test(normalized)
|
|
304
|
+
|| normalized.includes('authorization_uri=')
|
|
305
|
+
|| /\bauthorize\b/.test(normalized)
|
|
306
|
+
|| /\blogin\b/.test(normalized)
|
|
307
|
+
|| /\bsignin\b/.test(normalized)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function pickOAuthUrl(...candidates: Array<string | undefined>): string | undefined {
|
|
312
|
+
for (const value of candidates) {
|
|
313
|
+
if (!value) continue
|
|
314
|
+
const normalized = value.toLowerCase()
|
|
315
|
+
if (
|
|
316
|
+
/\boauth\b/.test(normalized)
|
|
317
|
+
|| /\bauthorize\b/.test(normalized)
|
|
318
|
+
|| /\blogin\b/.test(normalized)
|
|
319
|
+
|| /\bsignin\b/.test(normalized)
|
|
320
|
+
) {
|
|
321
|
+
return value
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return undefined
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function extractAuthParam(header: string, name: string): string | undefined {
|
|
328
|
+
if (!header) return undefined
|
|
329
|
+
const pattern = new RegExp(`${name}=\"([^\"]+)\"`, 'i')
|
|
330
|
+
return header.match(pattern)?.[1]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function extractHeaderName(values: string[]): string | undefined {
|
|
334
|
+
for (const value of values) {
|
|
335
|
+
const matches = value.match(/\b(?:x-[a-z0-9-]+|authorization)\b/gi)
|
|
336
|
+
if (!matches) continue
|
|
337
|
+
const candidate = matches[0]
|
|
338
|
+
if (!candidate) continue
|
|
339
|
+
if (candidate.toLowerCase() === 'authorization') {
|
|
340
|
+
return 'Authorization'
|
|
341
|
+
}
|
|
342
|
+
return candidate
|
|
343
|
+
.split('-')
|
|
344
|
+
.map((part) => {
|
|
345
|
+
const normalized = part.toLowerCase()
|
|
346
|
+
if (normalized === 'api' || normalized === 'id' || normalized === 'url') {
|
|
347
|
+
return normalized.toUpperCase()
|
|
348
|
+
}
|
|
349
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1)
|
|
350
|
+
})
|
|
351
|
+
.join('-')
|
|
352
|
+
}
|
|
353
|
+
return undefined
|
|
354
|
+
}
|
|
355
|
+
|
|
156
356
|
async function listAllTools(client: McpClient): Promise<IntrospectedMcpTool[]> {
|
|
157
357
|
const tools: IntrospectedMcpTool[] = []
|
|
158
358
|
let cursor: string | undefined
|
|
@@ -167,6 +367,61 @@ async function listAllTools(client: McpClient): Promise<IntrospectedMcpTool[]> {
|
|
|
167
367
|
}
|
|
168
368
|
}
|
|
169
369
|
|
|
370
|
+
async function listAllResources(client: McpClient): Promise<IntrospectedMcpResource[]> {
|
|
371
|
+
return await listOptionalPaged(client, 'resources/list', (result: ListResourcesResult) => result.resources ?? [])
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function listAllResourceTemplates(client: McpClient): Promise<IntrospectedMcpResourceTemplate[]> {
|
|
375
|
+
return await listOptionalPaged(
|
|
376
|
+
client,
|
|
377
|
+
'resources/templates/list',
|
|
378
|
+
(result: ListResourceTemplatesResult) => result.resourceTemplates ?? [],
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function listAllPrompts(client: McpClient): Promise<IntrospectedMcpPrompt[]> {
|
|
383
|
+
return await listOptionalPaged(client, 'prompts/list', (result: ListPromptsResult) => result.prompts ?? [])
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function listOptionalPaged<TResult extends { nextCursor?: string }, TItem>(
|
|
387
|
+
client: McpClient,
|
|
388
|
+
method: string,
|
|
389
|
+
getItems: (result: TResult) => TItem[],
|
|
390
|
+
): Promise<TItem[]> {
|
|
391
|
+
const items: TItem[] = []
|
|
392
|
+
let cursor: string | undefined
|
|
393
|
+
|
|
394
|
+
while (true) {
|
|
395
|
+
let result: TResult
|
|
396
|
+
try {
|
|
397
|
+
result = await client.request<TResult>(method, cursor ? { cursor } : undefined)
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isOptionalDiscoveryUnavailable(error)) {
|
|
400
|
+
return items
|
|
401
|
+
}
|
|
402
|
+
throw error
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
items.push(...getItems(result))
|
|
406
|
+
cursor = result.nextCursor
|
|
407
|
+
if (!cursor) {
|
|
408
|
+
return items
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isOptionalDiscoveryUnavailable(error: unknown): boolean {
|
|
414
|
+
if (!(error instanceof McpIntrospectionError)) {
|
|
415
|
+
return false
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const message = error.message.toLowerCase()
|
|
419
|
+
return error.rpcCode === -32601
|
|
420
|
+
|| message.includes('method not found')
|
|
421
|
+
|| message.includes('not supported')
|
|
422
|
+
|| message.includes('unsupported')
|
|
423
|
+
}
|
|
424
|
+
|
|
170
425
|
function createHttpClient(server: Exclude<McpServer, { transport: 'stdio' }>): McpClient {
|
|
171
426
|
let sessionId: string | null = null
|
|
172
427
|
|
|
@@ -541,6 +796,7 @@ async function createStdioClient(server: Extract<McpServer, { transport: 'stdio'
|
|
|
541
796
|
},
|
|
542
797
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
543
798
|
})
|
|
799
|
+
let isClosing = false
|
|
544
800
|
|
|
545
801
|
const pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>()
|
|
546
802
|
const stdout = readline.createInterface({
|
|
@@ -578,6 +834,10 @@ async function createStdioClient(server: Extract<McpServer, { transport: 'stdio'
|
|
|
578
834
|
})
|
|
579
835
|
|
|
580
836
|
child.once('exit', (code, signal) => {
|
|
837
|
+
if (isClosing) {
|
|
838
|
+
pending.clear()
|
|
839
|
+
return
|
|
840
|
+
}
|
|
581
841
|
const error = new McpIntrospectionError(
|
|
582
842
|
`MCP stdio process exited before pluxx finished introspecting it (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`,
|
|
583
843
|
)
|
|
@@ -637,8 +897,16 @@ async function createStdioClient(server: Extract<McpServer, { transport: 'stdio'
|
|
|
637
897
|
},
|
|
638
898
|
|
|
639
899
|
async close(): Promise<void> {
|
|
900
|
+
isClosing = true
|
|
640
901
|
stdout.close()
|
|
641
|
-
child.
|
|
902
|
+
child.stdin.end()
|
|
903
|
+
child.stdout.destroy()
|
|
904
|
+
child.stderr?.destroy()
|
|
905
|
+
|
|
906
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
907
|
+
child.kill('SIGKILL')
|
|
908
|
+
child.unref()
|
|
909
|
+
}
|
|
642
910
|
},
|
|
643
911
|
}
|
|
644
912
|
}
|
|
@@ -821,7 +1089,12 @@ function parseSsePayload(payload: string): Array<JsonRpcEnvelope<unknown>> {
|
|
|
821
1089
|
|
|
822
1090
|
function unwrapEnvelope<T>(envelope: JsonRpcEnvelope<T>): T {
|
|
823
1091
|
if ('error' in envelope) {
|
|
824
|
-
throw new McpIntrospectionError(
|
|
1092
|
+
throw new McpIntrospectionError(
|
|
1093
|
+
`MCP request failed: ${envelope.error.message}`,
|
|
1094
|
+
undefined,
|
|
1095
|
+
undefined,
|
|
1096
|
+
envelope.error.code,
|
|
1097
|
+
)
|
|
825
1098
|
}
|
|
826
1099
|
|
|
827
1100
|
return envelope.result
|
package/src/permissions.ts
CHANGED
|
@@ -84,7 +84,9 @@ export function buildOpenCodePermissionMap(
|
|
|
84
84
|
Edit: ['edit', 'write'],
|
|
85
85
|
Read: ['read'],
|
|
86
86
|
MCP: ['mcp'],
|
|
87
|
-
|
|
87
|
+
// OpenCode's native permission surface is tool-level and does not expose
|
|
88
|
+
// a dedicated skill permission key.
|
|
89
|
+
Skill: [],
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
for (const rule of rules) {
|
|
@@ -7,6 +7,13 @@ export interface PlatformRuleSource {
|
|
|
7
7
|
url: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export type PlatformLimitKind = 'hard' | 'advisory' | 'display'
|
|
11
|
+
|
|
12
|
+
export interface PlatformLimitPolicy {
|
|
13
|
+
kind: PlatformLimitKind
|
|
14
|
+
notes?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export interface PlatformLimits {
|
|
11
18
|
skillDescriptionMax: number | null
|
|
12
19
|
skillDescriptionDisplayMax: number | null
|
|
@@ -21,10 +28,25 @@ export interface PlatformLimits {
|
|
|
21
28
|
rulesMaxLines: number | null
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
export interface PlatformLimitPolicies {
|
|
32
|
+
skillDescriptionMax: PlatformLimitPolicy | null
|
|
33
|
+
skillDescriptionDisplayMax: PlatformLimitPolicy | null
|
|
34
|
+
skillListingBudgetMax: PlatformLimitPolicy | null
|
|
35
|
+
skillNameMax: PlatformLimitPolicy
|
|
36
|
+
skillNameMustMatchDir: PlatformLimitPolicy
|
|
37
|
+
manifestPromptMax: PlatformLimitPolicy | null
|
|
38
|
+
manifestPromptCountMax: PlatformLimitPolicy | null
|
|
39
|
+
manifestPathPrefix: PlatformLimitPolicy | null
|
|
40
|
+
instructionsMaxBytes: PlatformLimitPolicy | null
|
|
41
|
+
hooksFeatureFlag: PlatformLimitPolicy | null
|
|
42
|
+
rulesMaxLines: PlatformLimitPolicy | null
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
export interface PlatformRules {
|
|
25
46
|
platform: TargetPlatform
|
|
26
47
|
summary: string
|
|
27
48
|
limits: PlatformLimits
|
|
49
|
+
limitPolicies: PlatformLimitPolicies
|
|
28
50
|
skillDiscoveryDirs: {
|
|
29
51
|
path: string
|
|
30
52
|
level: RuleLevel
|
|
@@ -84,6 +106,20 @@ const NULL_LIMITS: PlatformLimits = {
|
|
|
84
106
|
rulesMaxLines: null,
|
|
85
107
|
}
|
|
86
108
|
|
|
109
|
+
const NULL_LIMIT_POLICIES: PlatformLimitPolicies = {
|
|
110
|
+
skillDescriptionMax: null,
|
|
111
|
+
skillDescriptionDisplayMax: null,
|
|
112
|
+
skillListingBudgetMax: null,
|
|
113
|
+
skillNameMax: { kind: 'hard' },
|
|
114
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
115
|
+
manifestPromptMax: null,
|
|
116
|
+
manifestPromptCountMax: null,
|
|
117
|
+
manifestPathPrefix: null,
|
|
118
|
+
instructionsMaxBytes: null,
|
|
119
|
+
hooksFeatureFlag: null,
|
|
120
|
+
rulesMaxLines: null,
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
export const PLATFORM_LIMITS: Record<TargetPlatform, PlatformLimits> = {
|
|
88
124
|
'claude-code': {
|
|
89
125
|
...NULL_LIMITS,
|
|
@@ -138,6 +174,78 @@ export const PLATFORM_LIMITS: Record<TargetPlatform, PlatformLimits> = {
|
|
|
138
174
|
},
|
|
139
175
|
}
|
|
140
176
|
|
|
177
|
+
export const PLATFORM_LIMIT_POLICIES: Record<TargetPlatform, PlatformLimitPolicies> = {
|
|
178
|
+
'claude-code': {
|
|
179
|
+
...NULL_LIMIT_POLICIES,
|
|
180
|
+
skillDescriptionMax: {
|
|
181
|
+
kind: 'hard',
|
|
182
|
+
notes: 'Claude skills listing caps description + when_to_use combined at 1,536 characters.',
|
|
183
|
+
},
|
|
184
|
+
skillDescriptionDisplayMax: {
|
|
185
|
+
kind: 'display',
|
|
186
|
+
notes: 'Claude surfaces commonly truncate long listing text around 250 characters.',
|
|
187
|
+
},
|
|
188
|
+
skillListingBudgetMax: {
|
|
189
|
+
kind: 'advisory',
|
|
190
|
+
notes: 'Pluxx warns at 8,000 aggregate characters to keep Claude listings readable and avoid crowded discovery surfaces.',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
'codex': {
|
|
194
|
+
...NULL_LIMIT_POLICIES,
|
|
195
|
+
skillDescriptionMax: { kind: 'hard' },
|
|
196
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
197
|
+
manifestPromptMax: { kind: 'hard' },
|
|
198
|
+
manifestPromptCountMax: { kind: 'hard' },
|
|
199
|
+
manifestPathPrefix: { kind: 'hard' },
|
|
200
|
+
instructionsMaxBytes: {
|
|
201
|
+
kind: 'hard',
|
|
202
|
+
notes: 'Codex AGENTS.md/project docs truncate at 32 KiB.',
|
|
203
|
+
},
|
|
204
|
+
hooksFeatureFlag: {
|
|
205
|
+
kind: 'hard',
|
|
206
|
+
notes: 'Hook support depends on the Codex hooks feature flag/runtime support.',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
'cursor': {
|
|
210
|
+
...NULL_LIMIT_POLICIES,
|
|
211
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
212
|
+
rulesMaxLines: {
|
|
213
|
+
kind: 'advisory',
|
|
214
|
+
notes: 'Cursor docs treat 500 lines as practical guidance rather than a documented hard cap.',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
'opencode': {
|
|
218
|
+
...NULL_LIMIT_POLICIES,
|
|
219
|
+
skillDescriptionMax: { kind: 'hard' },
|
|
220
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
221
|
+
},
|
|
222
|
+
'github-copilot': {
|
|
223
|
+
...NULL_LIMIT_POLICIES,
|
|
224
|
+
skillDescriptionDisplayMax: { kind: 'display' },
|
|
225
|
+
},
|
|
226
|
+
'openhands': {
|
|
227
|
+
...NULL_LIMIT_POLICIES,
|
|
228
|
+
},
|
|
229
|
+
'warp': {
|
|
230
|
+
...NULL_LIMIT_POLICIES,
|
|
231
|
+
},
|
|
232
|
+
'gemini-cli': {
|
|
233
|
+
...NULL_LIMIT_POLICIES,
|
|
234
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
235
|
+
},
|
|
236
|
+
'roo-code': {
|
|
237
|
+
...NULL_LIMIT_POLICIES,
|
|
238
|
+
},
|
|
239
|
+
'cline': {
|
|
240
|
+
...NULL_LIMIT_POLICIES,
|
|
241
|
+
skillDescriptionMax: { kind: 'hard' },
|
|
242
|
+
skillNameMustMatchDir: { kind: 'hard' },
|
|
243
|
+
},
|
|
244
|
+
'amp': {
|
|
245
|
+
...NULL_LIMIT_POLICIES,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
141
249
|
type ResearchTarget = Extract<
|
|
142
250
|
TargetPlatform,
|
|
143
251
|
| 'claude-code'
|
|
@@ -157,6 +265,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
157
265
|
platform: 'claude-code',
|
|
158
266
|
summary: 'Claude Code plugins use an optional manifest at .claude-plugin/plugin.json with auto-discovery for skills, commands, agents, hooks, MCP, and output styles.',
|
|
159
267
|
limits: PLATFORM_LIMITS['claude-code'],
|
|
268
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['claude-code'],
|
|
160
269
|
skillDiscoveryDirs: [
|
|
161
270
|
{ path: 'skills/', level: 'supported' },
|
|
162
271
|
],
|
|
@@ -187,6 +296,9 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
187
296
|
format: 'markdown',
|
|
188
297
|
},
|
|
189
298
|
sources: [
|
|
299
|
+
{ label: 'Claude Code headless docs', url: 'https://code.claude.com/docs/en/headless' },
|
|
300
|
+
{ label: 'Claude Code CLI reference', url: 'https://code.claude.com/docs/en/cli-reference' },
|
|
301
|
+
{ label: 'Claude Code discover plugins docs', url: 'https://code.claude.com/docs/en/discover-plugins' },
|
|
190
302
|
{ label: 'Claude Code plugins reference', url: 'https://code.claude.com/docs/en/plugins-reference' },
|
|
191
303
|
{ label: 'Claude Code hooks docs', url: 'https://code.claude.com/docs/en/hooks' },
|
|
192
304
|
{ label: 'Claude Code skills docs', url: 'https://code.claude.com/docs/en/skills' },
|
|
@@ -196,6 +308,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
196
308
|
platform: 'cursor',
|
|
197
309
|
summary: 'Cursor plugins use .cursor-plugin/plugin.json plus auto-discovered rules, skills, agents, commands, hooks, and mcp.json at the plugin root; Cursor subagents are a related but separate surface under .cursor/agents and ~/.cursor/agents.',
|
|
198
310
|
limits: PLATFORM_LIMITS['cursor'],
|
|
311
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['cursor'],
|
|
199
312
|
skillDiscoveryDirs: [
|
|
200
313
|
{ path: 'skills/', level: 'supported' },
|
|
201
314
|
{ path: 'SKILL.md', level: 'fallback', notes: 'Used when no skills directory or manifest skill path is present.' },
|
|
@@ -243,6 +356,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
243
356
|
platform: 'codex',
|
|
244
357
|
summary: 'Codex plugins use .codex-plugin/plugin.json with skills, .mcp.json, optional app mappings, and AGENTS.md; current docs separate plugin packaging from hooks configuration and do not document plugin-provided slash commands.',
|
|
245
358
|
limits: PLATFORM_LIMITS['codex'],
|
|
359
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['codex'],
|
|
246
360
|
skillDiscoveryDirs: [
|
|
247
361
|
{ path: 'skills/', level: 'supported' },
|
|
248
362
|
],
|
|
@@ -284,6 +398,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
284
398
|
platform: 'opencode',
|
|
285
399
|
summary: 'OpenCode plugins are code-first TypeScript or JavaScript modules that register skills, commands, MCP servers, and hook handlers programmatically.',
|
|
286
400
|
limits: PLATFORM_LIMITS['opencode'],
|
|
401
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['opencode'],
|
|
287
402
|
skillDiscoveryDirs: [
|
|
288
403
|
{ path: 'skills/', level: 'supported' },
|
|
289
404
|
],
|
|
@@ -323,6 +438,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
323
438
|
platform: 'openhands',
|
|
324
439
|
summary: 'OpenHands plugins use a Claude-style manifest at .plugin/plugin.json and support skills, hooks, and MCP.',
|
|
325
440
|
limits: PLATFORM_LIMITS['openhands'],
|
|
441
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['openhands'],
|
|
326
442
|
skillDiscoveryDirs: [
|
|
327
443
|
{ path: '.openhands/skills/', level: 'supported' },
|
|
328
444
|
{ path: '.claude/skills/', level: 'supported' },
|
|
@@ -363,6 +479,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
363
479
|
platform: 'warp',
|
|
364
480
|
summary: 'Warp supports skills, rules, and MCP with AGENTS.md as the current rules anchor.',
|
|
365
481
|
limits: PLATFORM_LIMITS['warp'],
|
|
482
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['warp'],
|
|
366
483
|
skillDiscoveryDirs: [
|
|
367
484
|
{ path: '.agents/skills/', level: 'supported' },
|
|
368
485
|
{ path: '.warp/skills/', level: 'supported' },
|
|
@@ -405,6 +522,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
405
522
|
platform: 'gemini-cli',
|
|
406
523
|
summary: 'Gemini CLI uses gemini-extension.json, GEMINI.md instructions, and hook definitions in hooks/hooks.json.',
|
|
407
524
|
limits: PLATFORM_LIMITS['gemini-cli'],
|
|
525
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['gemini-cli'],
|
|
408
526
|
skillDiscoveryDirs: [
|
|
409
527
|
{ path: 'skills/', level: 'supported' },
|
|
410
528
|
],
|
|
@@ -443,6 +561,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
443
561
|
platform: 'roo-code',
|
|
444
562
|
summary: 'Roo Code supports project and mode-specific rules, project-level MCP config, and custom modes metadata.',
|
|
445
563
|
limits: PLATFORM_LIMITS['roo-code'],
|
|
564
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['roo-code'],
|
|
446
565
|
skillDiscoveryDirs: [
|
|
447
566
|
{ path: '.roo/skills/', level: 'supported' },
|
|
448
567
|
],
|
|
@@ -484,6 +603,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
484
603
|
platform: 'cline',
|
|
485
604
|
summary: 'Cline supports layered rules, .cline/mcp.json, and conditional rules via frontmatter path globs.',
|
|
486
605
|
limits: PLATFORM_LIMITS['cline'],
|
|
606
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['cline'],
|
|
487
607
|
skillDiscoveryDirs: [
|
|
488
608
|
{ path: '.cline/skills/', level: 'supported' },
|
|
489
609
|
{ path: '.agents/skills/', level: 'supported' },
|
|
@@ -524,6 +644,7 @@ export const PLATFORM_VALIDATION_RULES: Record<ResearchTarget, PlatformRules> =
|
|
|
524
644
|
platform: 'amp',
|
|
525
645
|
summary: 'AMP uses AGENTS.md/AGENT.md for instruction hierarchy and .amp/settings.json for settings, hooks, and MCP.',
|
|
526
646
|
limits: PLATFORM_LIMITS['amp'],
|
|
647
|
+
limitPolicies: PLATFORM_LIMIT_POLICIES['amp'],
|
|
527
648
|
skillDiscoveryDirs: [
|
|
528
649
|
{ path: '.agents/skills/', level: 'supported' },
|
|
529
650
|
{ path: '~/.config/amp/skills/', level: 'supported' },
|