@orchid-labs/pluxx 0.1.1 → 0.1.4
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 +25 -8
- package/bin/pluxx.js +19 -28
- package/dist/agents.d.ts +16 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/cli/agent.d.ts +62 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21810 -0
- package/dist/cli/init-from-mcp.d.ts +17 -1
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +3 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts +14 -0
- package/dist/cli/primitive-summary.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +1 -1
- package/dist/cli/publish.d.ts +6 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts +25 -0
- package/dist/cli/verify-install.d.ts.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/compiler-intent.d.ts +165 -0
- package/dist/compiler-intent.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/delegation.d.ts +11 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts.map +1 -1
- package/dist/generators/base.d.ts +5 -0
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/cline/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +4 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/generators/cursor/index.d.ts +1 -0
- package/dist/generators/cursor/index.d.ts.map +1 -1
- package/dist/generators/gemini-cli/index.d.ts.map +1 -1
- package/dist/generators/github-copilot/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts +1 -0
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/generators/openhands/index.d.ts.map +1 -1
- package/dist/generators/roo-code/index.d.ts.map +1 -1
- package/dist/generators/shared/claude-family.d.ts.map +1 -1
- package/dist/generators/warp/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5371 -553
- package/dist/schema.d.ts +91 -42
- package/dist/schema.d.ts.map +1 -1
- package/dist/text-files.d.ts +5 -0
- package/dist/text-files.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +15 -1
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +15 -13
- package/src/cli/agent.ts +0 -1455
- package/src/cli/dev.ts +0 -112
- package/src/cli/doctor.ts +0 -987
- package/src/cli/eval.ts +0 -470
- package/src/cli/index.ts +0 -2933
- package/src/cli/init-from-mcp.ts +0 -2115
- package/src/cli/install.ts +0 -860
- package/src/cli/lint.ts +0 -1249
- package/src/cli/mcp-proxy.ts +0 -322
- package/src/cli/migrate.ts +0 -867
- package/src/cli/prompt.ts +0 -82
- package/src/cli/publish.ts +0 -401
- package/src/cli/runtime.ts +0 -86
- package/src/cli/sync-from-mcp.ts +0 -586
- package/src/cli/test.ts +0 -142
- package/src/compatibility/matrix.ts +0 -149
- package/src/config/define.ts +0 -20
- package/src/config/load.ts +0 -74
- package/src/generators/amp/index.ts +0 -63
- package/src/generators/base.ts +0 -188
- package/src/generators/claude-code/index.ts +0 -172
- package/src/generators/cline/index.ts +0 -35
- package/src/generators/codex/index.ts +0 -143
- package/src/generators/cursor/index.ts +0 -158
- package/src/generators/gemini-cli/index.ts +0 -83
- package/src/generators/github-copilot/index.ts +0 -32
- package/src/generators/hooks-warning.ts +0 -51
- package/src/generators/index.ts +0 -71
- package/src/generators/opencode/index.ts +0 -526
- package/src/generators/openhands/index.ts +0 -32
- package/src/generators/roo-code/index.ts +0 -35
- package/src/generators/shared/claude-family.ts +0 -215
- package/src/generators/warp/index.ts +0 -32
- package/src/hook-events.ts +0 -33
- package/src/index.ts +0 -34
- package/src/mcp/introspect.ts +0 -1107
- package/src/permissions.ts +0 -260
- package/src/schema.ts +0 -312
- package/src/user-config.ts +0 -177
- package/src/validation/platform-rules.ts +0 -686
package/src/cli/init-from-mcp.ts
DELETED
|
@@ -1,2115 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { mkdir } from 'fs/promises'
|
|
3
|
-
import { basename, resolve } from 'path'
|
|
4
|
-
import type { HookEntry, McpServer, PluginConfig, TargetPlatform, UserConfigEntry } from '../schema'
|
|
5
|
-
import type {
|
|
6
|
-
IntrospectedMcpPrompt,
|
|
7
|
-
IntrospectedMcpResource,
|
|
8
|
-
IntrospectedMcpResourceTemplate,
|
|
9
|
-
IntrospectedMcpServer,
|
|
10
|
-
IntrospectedMcpTool,
|
|
11
|
-
} from '../mcp/introspect'
|
|
12
|
-
import { collectUserConfigEntries } from '../user-config'
|
|
13
|
-
|
|
14
|
-
export interface McpScaffoldOptions {
|
|
15
|
-
rootDir: string
|
|
16
|
-
pluginName: string
|
|
17
|
-
authorName: string
|
|
18
|
-
targets: TargetPlatform[]
|
|
19
|
-
source: McpServer
|
|
20
|
-
introspection: IntrospectedMcpServer
|
|
21
|
-
serverName?: string
|
|
22
|
-
displayName?: string
|
|
23
|
-
description?: string
|
|
24
|
-
skillGrouping?: McpSkillGrouping
|
|
25
|
-
hookMode?: McpHookMode
|
|
26
|
-
runtimeAuthMode?: McpRuntimeAuthMode
|
|
27
|
-
persistedSkills?: PersistedSkill[]
|
|
28
|
-
toolRenames?: Map<string, string>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface McpScaffoldResult {
|
|
32
|
-
instructionsPath: string
|
|
33
|
-
skillDirectories: string[]
|
|
34
|
-
commandFiles: string[]
|
|
35
|
-
generatedFiles: string[]
|
|
36
|
-
generatedHookMode: McpHookMode
|
|
37
|
-
generatedHookEvents: string[]
|
|
38
|
-
metadataPath: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface McpScaffoldPlannedFile {
|
|
42
|
-
relativePath: string
|
|
43
|
-
content: string
|
|
44
|
-
action: 'create' | 'update' | 'unchanged'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface McpScaffoldPlan extends McpScaffoldResult {
|
|
48
|
-
files: McpScaffoldPlannedFile[]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface PlannedSkill {
|
|
52
|
-
dirName: string
|
|
53
|
-
title: string
|
|
54
|
-
description: string
|
|
55
|
-
tools: IntrospectedMcpTool[]
|
|
56
|
-
resources: IntrospectedMcpResource[]
|
|
57
|
-
resourceTemplates: IntrospectedMcpResourceTemplate[]
|
|
58
|
-
prompts: IntrospectedMcpPrompt[]
|
|
59
|
-
workflowKey?: string | null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface PersistedSkill {
|
|
63
|
-
dirName: string
|
|
64
|
-
title: string
|
|
65
|
-
description?: string
|
|
66
|
-
toolNames: string[]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface SchemaField {
|
|
70
|
-
name: string
|
|
71
|
-
type: string
|
|
72
|
-
required: boolean
|
|
73
|
-
description?: string
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type McpQualityLevel = 'warning' | 'info'
|
|
77
|
-
|
|
78
|
-
export interface McpQualityIssue {
|
|
79
|
-
level: McpQualityLevel
|
|
80
|
-
code: string
|
|
81
|
-
title: string
|
|
82
|
-
detail: string
|
|
83
|
-
fix: string
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface McpQualityReport {
|
|
87
|
-
ok: boolean
|
|
88
|
-
warnings: number
|
|
89
|
-
infos: number
|
|
90
|
-
issues: McpQualityIssue[]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export const MCP_SKILL_GROUPINGS = ['workflow', 'tool'] as const
|
|
94
|
-
export type McpSkillGrouping = typeof MCP_SKILL_GROUPINGS[number]
|
|
95
|
-
|
|
96
|
-
export const MCP_HOOK_MODES = ['none', 'safe'] as const
|
|
97
|
-
export type McpHookMode = typeof MCP_HOOK_MODES[number]
|
|
98
|
-
export const MCP_RUNTIME_AUTH_MODES = ['inline', 'platform'] as const
|
|
99
|
-
export type McpRuntimeAuthMode = typeof MCP_RUNTIME_AUTH_MODES[number]
|
|
100
|
-
|
|
101
|
-
interface GeneratedHookScaffold {
|
|
102
|
-
mode: McpHookMode
|
|
103
|
-
scriptsPath?: string
|
|
104
|
-
hookEntries?: Record<string, HookEntry[]>
|
|
105
|
-
files: Array<{ relativePath: string; content: string }>
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface McpScaffoldMetadata {
|
|
109
|
-
version: 1
|
|
110
|
-
source: McpServer
|
|
111
|
-
serverInfo: IntrospectedMcpServer['serverInfo']
|
|
112
|
-
settings: {
|
|
113
|
-
pluginName: string
|
|
114
|
-
displayName: string
|
|
115
|
-
description?: string
|
|
116
|
-
skillGrouping: McpSkillGrouping
|
|
117
|
-
requestedHookMode: McpHookMode
|
|
118
|
-
generatedHookMode: McpHookMode
|
|
119
|
-
generatedHookEvents: string[]
|
|
120
|
-
runtimeAuthMode: McpRuntimeAuthMode
|
|
121
|
-
}
|
|
122
|
-
userConfig: UserConfigEntry[]
|
|
123
|
-
tools: IntrospectedMcpTool[]
|
|
124
|
-
resources?: IntrospectedMcpResource[]
|
|
125
|
-
resourceTemplates?: IntrospectedMcpResourceTemplate[]
|
|
126
|
-
prompts?: IntrospectedMcpPrompt[]
|
|
127
|
-
skills: Array<{
|
|
128
|
-
dirName: string
|
|
129
|
-
title: string
|
|
130
|
-
description?: string
|
|
131
|
-
toolNames: string[]
|
|
132
|
-
resourceUris?: string[]
|
|
133
|
-
resourceTemplateUris?: string[]
|
|
134
|
-
promptNames?: string[]
|
|
135
|
-
}>
|
|
136
|
-
managedFiles: string[]
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export const MCP_SCAFFOLD_METADATA_PATH = '.pluxx/mcp.json'
|
|
140
|
-
export const MCP_TAXONOMY_PATH = '.pluxx/taxonomy.json'
|
|
141
|
-
export const PLUXX_GENERATED_START = '<!-- pluxx:generated:start -->'
|
|
142
|
-
export const PLUXX_GENERATED_END = '<!-- pluxx:generated:end -->'
|
|
143
|
-
export const PLUXX_CUSTOM_START = '<!-- pluxx:custom:start -->'
|
|
144
|
-
export const PLUXX_CUSTOM_END = '<!-- pluxx:custom:end -->'
|
|
145
|
-
|
|
146
|
-
const DEFAULT_INSTRUCTIONS_CUSTOM_CONTENT = 'Add custom plugin instructions here. This section is preserved across `pluxx sync --from-mcp`.'
|
|
147
|
-
const DEFAULT_SKILL_CUSTOM_CONTENT = 'Add custom guidance, examples, or caveats here. This section is preserved across `pluxx sync --from-mcp`.'
|
|
148
|
-
const LEGACY_MIXED_CONTENT_NOTE = 'Migrated from a previous unstructured scaffold. Review and trim this section as needed.'
|
|
149
|
-
|
|
150
|
-
interface MixedMarkdownContent {
|
|
151
|
-
hasMarkers: boolean
|
|
152
|
-
customContent: string
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const WORKFLOW_SKILL_DEFINITIONS = [
|
|
156
|
-
{
|
|
157
|
-
key: 'setup-and-auth',
|
|
158
|
-
title: 'Setup and Auth',
|
|
159
|
-
description: 'Confirm access, auth state, and session readiness before running operational workflows.',
|
|
160
|
-
match: ['connect', 'connected', 'connection', 'status', 'auth', 'session', 'cookie', 'workspace'],
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
key: 'workflow-design',
|
|
164
|
-
title: 'Workflow Design',
|
|
165
|
-
description: 'Define strategy, prompts, targeting, and workflow shape before building tables or running enrichments.',
|
|
166
|
-
match: ['workflow', 'design', 'play', 'prompt', 'icp', 'persona', 'audience', 'segment', 'brainstorm', 'outreach'],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
key: 'account-research',
|
|
170
|
-
title: 'Account Research',
|
|
171
|
-
description: 'Research companies, organizations, and account context before taking action.',
|
|
172
|
-
match: ['organization', 'organisation', 'company', 'account', 'firmographic'],
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
key: 'contact-discovery',
|
|
176
|
-
title: 'Contact Discovery',
|
|
177
|
-
description: 'Find people, contacts, and buyer-side context at the right accounts.',
|
|
178
|
-
match: ['people', 'person', 'contact', 'prospect', 'decision maker', 'org chart'],
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
key: 'hiring-signals',
|
|
182
|
-
title: 'Hiring Signals',
|
|
183
|
-
description: 'Use hiring activity and open roles as timing signals for outreach and research.',
|
|
184
|
-
match: ['job', 'jobs', 'hiring', 'hire', 'role', 'roles', 'recruit', 'career'],
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
key: 'technographics',
|
|
188
|
-
title: 'Technographics',
|
|
189
|
-
description: 'Research technologies, tools, and stack adoption across target accounts.',
|
|
190
|
-
match: ['technology', 'technologies', 'tech', 'stack', 'tooling', 'software'],
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
key: 'table-operations',
|
|
194
|
-
title: 'Table Operations',
|
|
195
|
-
description: 'Build, inspect, run, document, and export tables, rows, and enrichment workflows.',
|
|
196
|
-
match: ['table', 'tables', 'schema', 'webhook', 'row', 'rows', 'export', 'audit', 'document', 'enrich', 'view'],
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
key: 'provider-research',
|
|
200
|
-
title: 'Provider Research',
|
|
201
|
-
description: 'Compare providers, integrations, and capability tradeoffs before choosing a workflow.',
|
|
202
|
-
match: ['provider', 'providers', 'integration', 'integrations', 'byoa', 'waterfall', 'compare', 'comparison'],
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
key: 'account-and-usage',
|
|
206
|
-
title: 'Account and Usage',
|
|
207
|
-
description: 'Check pricing, usage, limits, credits, and upgrade context for the current account.',
|
|
208
|
-
match: ['usage', 'tier', 'plan', 'pricing', 'price', 'cost', 'credits', 'checkout', 'upgrade', 'billing'],
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
key: 'general-research',
|
|
212
|
-
title: 'General Research',
|
|
213
|
-
description: 'Handle broad search and query workflows when there is not a more specific product surface match.',
|
|
214
|
-
match: ['search', 'query', 'lookup', 'look up', 'discover', 'find'],
|
|
215
|
-
},
|
|
216
|
-
] as const
|
|
217
|
-
|
|
218
|
-
const GENERIC_TOOL_NAMES = new Set([
|
|
219
|
-
'run',
|
|
220
|
-
'execute',
|
|
221
|
-
'query',
|
|
222
|
-
'invoke',
|
|
223
|
-
'action',
|
|
224
|
-
'tool',
|
|
225
|
-
'command',
|
|
226
|
-
'workflow',
|
|
227
|
-
'task',
|
|
228
|
-
])
|
|
229
|
-
|
|
230
|
-
export function parseMcpSourceInput(input: string, transportOverride?: string): McpServer {
|
|
231
|
-
const value = input.trim()
|
|
232
|
-
if (!value) {
|
|
233
|
-
throw new Error('Expected an MCP server URL or a local command.')
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (transportOverride && transportOverride !== 'http' && transportOverride !== 'sse') {
|
|
237
|
-
throw new Error('Transport must be one of: http, sse')
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const url = new URL(value)
|
|
242
|
-
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
243
|
-
const normalizedPath = url.pathname.replace(/\/+$/, '')
|
|
244
|
-
const transport = transportOverride === 'sse' || (!transportOverride && normalizedPath.endsWith('/sse'))
|
|
245
|
-
? 'sse' as const
|
|
246
|
-
: 'http' as const
|
|
247
|
-
return {
|
|
248
|
-
transport,
|
|
249
|
-
url: url.toString(),
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch {
|
|
253
|
-
// Not a URL, treat it as a stdio command.
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const parts = splitCommandString(value)
|
|
257
|
-
if (parts.length === 0) {
|
|
258
|
-
throw new Error('Expected an MCP server URL or a local command.')
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
transport: 'stdio',
|
|
263
|
-
command: parts[0],
|
|
264
|
-
args: parts.slice(1),
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export async function writeMcpScaffold(options: McpScaffoldOptions): Promise<McpScaffoldResult> {
|
|
269
|
-
const plan = await planMcpScaffold(options)
|
|
270
|
-
await applyMcpScaffoldPlan(options.rootDir, plan)
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
instructionsPath: plan.instructionsPath,
|
|
274
|
-
skillDirectories: plan.skillDirectories,
|
|
275
|
-
commandFiles: plan.commandFiles,
|
|
276
|
-
generatedFiles: plan.generatedFiles,
|
|
277
|
-
generatedHookMode: plan.generatedHookMode,
|
|
278
|
-
generatedHookEvents: plan.generatedHookEvents,
|
|
279
|
-
metadataPath: plan.metadataPath,
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function applyMcpScaffoldPlan(rootDir: string, plan: McpScaffoldPlan): Promise<void> {
|
|
284
|
-
for (const file of plan.files) {
|
|
285
|
-
const filePath = resolve(rootDir, file.relativePath)
|
|
286
|
-
const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
|
|
287
|
-
if (parentDir) {
|
|
288
|
-
await mkdir(resolve(rootDir, parentDir), { recursive: true })
|
|
289
|
-
}
|
|
290
|
-
await Bun.write(filePath, file.content)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function planMcpScaffold(options: McpScaffoldOptions): Promise<McpScaffoldPlan> {
|
|
295
|
-
const pluginName = toKebabCase(options.pluginName) || 'mcp-plugin'
|
|
296
|
-
const displayName = options.displayName
|
|
297
|
-
?? deriveDisplayName(options.introspection, pluginName)
|
|
298
|
-
const plannedSkills = planSkillScaffoldsFromPersisted(
|
|
299
|
-
options.introspection.tools,
|
|
300
|
-
options.skillGrouping,
|
|
301
|
-
options.introspection.resources ?? [],
|
|
302
|
-
options.introspection.resourceTemplates ?? [],
|
|
303
|
-
options.introspection.prompts ?? [],
|
|
304
|
-
options.persistedSkills,
|
|
305
|
-
options.toolRenames,
|
|
306
|
-
)
|
|
307
|
-
const description = options.description
|
|
308
|
-
?? deriveScaffoldDescription({
|
|
309
|
-
displayName,
|
|
310
|
-
introspection: options.introspection,
|
|
311
|
-
plannedSkills,
|
|
312
|
-
})
|
|
313
|
-
const serverName = options.serverName
|
|
314
|
-
?? toKebabCase(options.introspection.serverInfo.name)
|
|
315
|
-
?? pluginName
|
|
316
|
-
const runtimeAuthMode = options.runtimeAuthMode
|
|
317
|
-
?? (
|
|
318
|
-
options.source.transport !== 'stdio' && options.source.auth?.type === 'platform'
|
|
319
|
-
? 'platform'
|
|
320
|
-
: 'inline'
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
const instructionsPath = resolve(options.rootDir, 'INSTRUCTIONS.md')
|
|
324
|
-
const skillRoot = resolve(options.rootDir, 'skills')
|
|
325
|
-
const commandsRoot = resolve(options.rootDir, 'commands')
|
|
326
|
-
const userConfigSource = {
|
|
327
|
-
targets: options.targets,
|
|
328
|
-
mcp: {
|
|
329
|
-
[serverName]: options.source,
|
|
330
|
-
},
|
|
331
|
-
platforms: runtimeAuthMode === 'platform'
|
|
332
|
-
? {
|
|
333
|
-
'claude-code': { mcpAuth: 'platform' as const },
|
|
334
|
-
cursor: { mcpAuth: 'platform' as const },
|
|
335
|
-
}
|
|
336
|
-
: undefined,
|
|
337
|
-
} as PluginConfig
|
|
338
|
-
const userConfig = collectUserConfigEntries(userConfigSource)
|
|
339
|
-
.map(({ source: _source, ...entry }) => entry)
|
|
340
|
-
const skillDirectories: string[] = []
|
|
341
|
-
const commandFiles: string[] = []
|
|
342
|
-
const generatedFiles = ['pluxx.config.ts', './INSTRUCTIONS.md']
|
|
343
|
-
const generatedHooks = planGeneratedHooks(options.source, options.introspection.tools, serverName, options.hookMode)
|
|
344
|
-
const metadataPath = MCP_SCAFFOLD_METADATA_PATH
|
|
345
|
-
const taxonomyPath = MCP_TAXONOMY_PATH
|
|
346
|
-
const files: McpScaffoldPlannedFile[] = []
|
|
347
|
-
|
|
348
|
-
const addPlannedFile = async (relativePath: string, content: string) => {
|
|
349
|
-
const filePath = resolve(options.rootDir, relativePath)
|
|
350
|
-
const action = existsSync(filePath)
|
|
351
|
-
? ((await Bun.file(filePath).text()) === content ? 'unchanged' : 'update')
|
|
352
|
-
: 'create'
|
|
353
|
-
files.push({ relativePath, content, action })
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
await addPlannedFile(
|
|
357
|
-
'pluxx.config.ts',
|
|
358
|
-
buildConfigTemplate({
|
|
359
|
-
pluginName,
|
|
360
|
-
authorName: options.authorName,
|
|
361
|
-
description,
|
|
362
|
-
displayName,
|
|
363
|
-
serverName,
|
|
364
|
-
source: options.source,
|
|
365
|
-
websiteUrl: options.introspection.serverInfo.websiteUrl,
|
|
366
|
-
targets: options.targets,
|
|
367
|
-
userConfig,
|
|
368
|
-
hooks: generatedHooks.hookEntries,
|
|
369
|
-
scriptsPath: generatedHooks.scriptsPath,
|
|
370
|
-
runtimeAuthMode,
|
|
371
|
-
commandsPath: './commands/',
|
|
372
|
-
}),
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
await addPlannedFile(
|
|
376
|
-
'./INSTRUCTIONS.md',
|
|
377
|
-
wrapManagedMarkdown(
|
|
378
|
-
buildInstructionsContent({
|
|
379
|
-
displayName,
|
|
380
|
-
description,
|
|
381
|
-
source: options.source,
|
|
382
|
-
runtimeAuthMode,
|
|
383
|
-
instructions: options.introspection.instructions,
|
|
384
|
-
skills: plannedSkills,
|
|
385
|
-
tools: options.introspection.tools,
|
|
386
|
-
resources: options.introspection.resources ?? [],
|
|
387
|
-
resourceTemplates: options.introspection.resourceTemplates ?? [],
|
|
388
|
-
prompts: options.introspection.prompts ?? [],
|
|
389
|
-
userConfig,
|
|
390
|
-
}),
|
|
391
|
-
existsSync(instructionsPath) ? await Bun.file(instructionsPath).text() : undefined,
|
|
392
|
-
{
|
|
393
|
-
customHeading: '## Custom Instructions',
|
|
394
|
-
defaultCustomContent: DEFAULT_INSTRUCTIONS_CUSTOM_CONTENT,
|
|
395
|
-
},
|
|
396
|
-
),
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
for (const skill of plannedSkills) {
|
|
400
|
-
const relativeSkillPath = `skills/${skill.dirName}`
|
|
401
|
-
const skillPath = resolve(skillRoot, skill.dirName, 'SKILL.md')
|
|
402
|
-
const relativeCommandPath = `commands/${skill.dirName}.md`
|
|
403
|
-
const commandPath = resolve(commandsRoot, `${skill.dirName}.md`)
|
|
404
|
-
await addPlannedFile(
|
|
405
|
-
`${relativeSkillPath}/SKILL.md`,
|
|
406
|
-
wrapManagedMarkdown(
|
|
407
|
-
buildSkillContent(skill),
|
|
408
|
-
existsSync(skillPath) ? await Bun.file(skillPath).text() : undefined,
|
|
409
|
-
{
|
|
410
|
-
customHeading: '## Custom Notes',
|
|
411
|
-
defaultCustomContent: DEFAULT_SKILL_CUSTOM_CONTENT,
|
|
412
|
-
},
|
|
413
|
-
),
|
|
414
|
-
)
|
|
415
|
-
skillDirectories.push(relativeSkillPath)
|
|
416
|
-
generatedFiles.push(`${relativeSkillPath}/SKILL.md`)
|
|
417
|
-
|
|
418
|
-
await addPlannedFile(
|
|
419
|
-
relativeCommandPath,
|
|
420
|
-
buildCommandContent(
|
|
421
|
-
skill,
|
|
422
|
-
existsSync(commandPath) ? await Bun.file(commandPath).text() : undefined,
|
|
423
|
-
),
|
|
424
|
-
)
|
|
425
|
-
commandFiles.push(relativeCommandPath)
|
|
426
|
-
generatedFiles.push(relativeCommandPath)
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
for (const file of generatedHooks.files) {
|
|
430
|
-
await addPlannedFile(file.relativePath, file.content)
|
|
431
|
-
generatedFiles.push(file.relativePath)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const metadata = buildMcpScaffoldMetadata({
|
|
435
|
-
source: options.source,
|
|
436
|
-
introspection: options.introspection,
|
|
437
|
-
pluginName,
|
|
438
|
-
displayName,
|
|
439
|
-
skillGrouping: options.skillGrouping ?? 'workflow',
|
|
440
|
-
requestedHookMode: options.hookMode ?? 'none',
|
|
441
|
-
generatedHookMode: generatedHooks.mode,
|
|
442
|
-
generatedHookEvents: Object.keys(generatedHooks.hookEntries ?? {}),
|
|
443
|
-
runtimeAuthMode,
|
|
444
|
-
userConfig,
|
|
445
|
-
plannedSkills,
|
|
446
|
-
managedFiles: [...generatedFiles, taxonomyPath, metadataPath],
|
|
447
|
-
})
|
|
448
|
-
await addPlannedFile(taxonomyPath, `${JSON.stringify(buildPersistedTaxonomy(plannedSkills), null, 2)}\n`)
|
|
449
|
-
generatedFiles.push(taxonomyPath)
|
|
450
|
-
await addPlannedFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`)
|
|
451
|
-
generatedFiles.push(metadataPath)
|
|
452
|
-
|
|
453
|
-
return {
|
|
454
|
-
instructionsPath: './INSTRUCTIONS.md',
|
|
455
|
-
skillDirectories,
|
|
456
|
-
commandFiles,
|
|
457
|
-
generatedFiles,
|
|
458
|
-
generatedHookMode: generatedHooks.mode,
|
|
459
|
-
generatedHookEvents: Object.keys(generatedHooks.hookEntries ?? {}),
|
|
460
|
-
metadataPath,
|
|
461
|
-
files,
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function wrapManagedMarkdown(
|
|
466
|
-
generatedContent: string,
|
|
467
|
-
existingContent: string | undefined,
|
|
468
|
-
options: {
|
|
469
|
-
customHeading: string
|
|
470
|
-
defaultCustomContent: string
|
|
471
|
-
},
|
|
472
|
-
): string {
|
|
473
|
-
const mixedContent = extractMixedMarkdownContent(existingContent, options.defaultCustomContent)
|
|
474
|
-
const customContent = mixedContent.customContent.trim() || options.defaultCustomContent
|
|
475
|
-
const { frontmatter, body } = splitMarkdownFrontmatter(generatedContent.trim())
|
|
476
|
-
|
|
477
|
-
const lines = [
|
|
478
|
-
...(frontmatter ? [frontmatter, ''] : []),
|
|
479
|
-
PLUXX_GENERATED_START,
|
|
480
|
-
body.trim(),
|
|
481
|
-
PLUXX_GENERATED_END,
|
|
482
|
-
'',
|
|
483
|
-
options.customHeading,
|
|
484
|
-
'',
|
|
485
|
-
PLUXX_CUSTOM_START,
|
|
486
|
-
customContent,
|
|
487
|
-
PLUXX_CUSTOM_END,
|
|
488
|
-
'',
|
|
489
|
-
]
|
|
490
|
-
|
|
491
|
-
return lines.join('\n')
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function splitMarkdownFrontmatter(content: string): { frontmatter: string; body: string } {
|
|
495
|
-
const lines = content.split(/\r?\n/)
|
|
496
|
-
if (lines[0]?.trim() !== '---') {
|
|
497
|
-
return {
|
|
498
|
-
frontmatter: '',
|
|
499
|
-
body: content,
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
let endIndex = -1
|
|
504
|
-
for (let index = 1; index < lines.length; index += 1) {
|
|
505
|
-
if (lines[index].trim() === '---') {
|
|
506
|
-
endIndex = index
|
|
507
|
-
break
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (endIndex === -1) {
|
|
512
|
-
return {
|
|
513
|
-
frontmatter: '',
|
|
514
|
-
body: content,
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
frontmatter: lines.slice(0, endIndex + 1).join('\n'),
|
|
520
|
-
body: lines.slice(endIndex + 1).join('\n').replace(/^\n+/, ''),
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export function extractMixedMarkdownContent(
|
|
525
|
-
content: string | undefined,
|
|
526
|
-
defaultCustomContent: string,
|
|
527
|
-
): MixedMarkdownContent {
|
|
528
|
-
if (!content) {
|
|
529
|
-
return {
|
|
530
|
-
hasMarkers: false,
|
|
531
|
-
customContent: defaultCustomContent,
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const customStart = content.indexOf(PLUXX_CUSTOM_START)
|
|
536
|
-
const customEnd = content.indexOf(PLUXX_CUSTOM_END)
|
|
537
|
-
|
|
538
|
-
if (customStart !== -1 && customEnd !== -1 && customEnd > customStart) {
|
|
539
|
-
const customContent = content
|
|
540
|
-
.slice(customStart + PLUXX_CUSTOM_START.length, customEnd)
|
|
541
|
-
.trim()
|
|
542
|
-
|
|
543
|
-
return {
|
|
544
|
-
hasMarkers: true,
|
|
545
|
-
customContent: customContent || defaultCustomContent,
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const trimmed = content.trim()
|
|
550
|
-
return {
|
|
551
|
-
hasMarkers: false,
|
|
552
|
-
customContent: trimmed
|
|
553
|
-
? `${LEGACY_MIXED_CONTENT_NOTE}\n\n${trimmed}`
|
|
554
|
-
: defaultCustomContent,
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function hasMeaningfulCustomContent(content: string | undefined): boolean {
|
|
559
|
-
if (!content) return false
|
|
560
|
-
|
|
561
|
-
const extracted = extractMixedMarkdownContent(content, DEFAULT_INSTRUCTIONS_CUSTOM_CONTENT)
|
|
562
|
-
const normalized = extracted.customContent.trim()
|
|
563
|
-
|
|
564
|
-
return normalized !== '' && normalized !== DEFAULT_INSTRUCTIONS_CUSTOM_CONTENT && normalized !== DEFAULT_SKILL_CUSTOM_CONTENT
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export function buildSkillContent(skill: PlannedSkill): string {
|
|
568
|
-
const description = buildSkillFrontmatterDescription(skill)
|
|
569
|
-
const exampleRequests = skill.tools
|
|
570
|
-
.map((tool) => buildToolExampleRequest(tool))
|
|
571
|
-
.filter((example, index, values) => values.indexOf(example) === index)
|
|
572
|
-
|
|
573
|
-
const lines = [
|
|
574
|
-
'---',
|
|
575
|
-
`name: ${JSON.stringify(skill.dirName)}`,
|
|
576
|
-
`description: ${JSON.stringify(description)}`,
|
|
577
|
-
'---',
|
|
578
|
-
'',
|
|
579
|
-
`# ${skill.title}`,
|
|
580
|
-
'',
|
|
581
|
-
skill.description,
|
|
582
|
-
'',
|
|
583
|
-
'## Tools In This Skill',
|
|
584
|
-
'',
|
|
585
|
-
]
|
|
586
|
-
|
|
587
|
-
for (const tool of skill.tools) {
|
|
588
|
-
lines.push(`### \`${tool.name}\``)
|
|
589
|
-
lines.push('')
|
|
590
|
-
lines.push(tool.description ?? `Calls \`${tool.name}\` on the configured MCP server.`)
|
|
591
|
-
lines.push('')
|
|
592
|
-
|
|
593
|
-
const fields = getTopLevelSchemaFields(tool.inputSchema)
|
|
594
|
-
if (fields.length > 0) {
|
|
595
|
-
lines.push('Inputs:')
|
|
596
|
-
for (const field of fields) {
|
|
597
|
-
const required = field.required ? ', required' : ''
|
|
598
|
-
const detail = field.description ? `: ${field.description}` : ''
|
|
599
|
-
lines.push(`- \`${field.name}\` (${field.type}${required})${detail}`)
|
|
600
|
-
}
|
|
601
|
-
lines.push('')
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (skill.resources.length > 0 || skill.resourceTemplates.length > 0) {
|
|
606
|
-
lines.push('## Related Resources', '')
|
|
607
|
-
|
|
608
|
-
for (const resource of skill.resources) {
|
|
609
|
-
const label = resource.name ?? resource.title ?? resource.uri
|
|
610
|
-
lines.push(`- \`${label}\`: ${summarizeResourceForInstructions(resource)}`)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
for (const template of skill.resourceTemplates) {
|
|
614
|
-
lines.push(`- \`${template.name}\`: ${summarizeResourceTemplateForInstructions(template)}`)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
lines.push('')
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (skill.prompts.length > 0) {
|
|
621
|
-
lines.push('## Related Prompt Templates', '')
|
|
622
|
-
|
|
623
|
-
for (const prompt of skill.prompts) {
|
|
624
|
-
lines.push(`- \`${prompt.name}\`: ${summarizePromptForInstructions(prompt)}`)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
lines.push('')
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
lines.push(
|
|
631
|
-
'## Example Requests',
|
|
632
|
-
'',
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
for (const example of exampleRequests) {
|
|
636
|
-
lines.push(`- "${example}"`)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
lines.push(
|
|
640
|
-
'',
|
|
641
|
-
'## Usage',
|
|
642
|
-
'',
|
|
643
|
-
'- Pick the most specific tool in this skill for the user request.',
|
|
644
|
-
'- Gather required inputs before calling a tool.',
|
|
645
|
-
'- Summarize the returned data clearly instead of dumping raw JSON unless the user asks for it.',
|
|
646
|
-
'',
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
return lines.join('\n')
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
export function buildCommandContent(skill: PlannedSkill, existingContent?: string): string {
|
|
653
|
-
const description = truncate(cleanSingleLineText(skill.description), 140)
|
|
654
|
-
const argumentHint = inferCommandArgumentHint(skill)
|
|
655
|
-
const entryBlurb = buildCommandEntryBlurb(skill)
|
|
656
|
-
const hasRelatedResources = skill.resources.length > 0 || skill.resourceTemplates.length > 0
|
|
657
|
-
const hasRelatedPrompts = skill.prompts.length > 0
|
|
658
|
-
const workflowSteps = [
|
|
659
|
-
'1. Interpret `$ARGUMENTS` as the user request for this workflow.',
|
|
660
|
-
'2. Choose the most specific tool in this surface.',
|
|
661
|
-
]
|
|
662
|
-
|
|
663
|
-
if (hasRelatedResources) {
|
|
664
|
-
workflowSteps.push(`${workflowSteps.length + 1}. Use related MCP resources or resource templates as canonical context when they fit the request.`)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (hasRelatedPrompts) {
|
|
668
|
-
workflowSteps.push(`${workflowSteps.length + 1}. Use related MCP prompt templates when they provide a canonical starting point for the task.`)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
workflowSteps.push(`${workflowSteps.length + 1}. Ask for missing required inputs only if the request does not already provide them.`)
|
|
672
|
-
workflowSteps.push(`${workflowSteps.length + 1}. Return a concise task-focused answer instead of raw JSON unless the user asks for it.`)
|
|
673
|
-
|
|
674
|
-
const generatedContent = [
|
|
675
|
-
'---',
|
|
676
|
-
`description: ${JSON.stringify(description)}`,
|
|
677
|
-
`argument-hint: ${JSON.stringify(argumentHint)}`,
|
|
678
|
-
'---',
|
|
679
|
-
'',
|
|
680
|
-
entryBlurb,
|
|
681
|
-
'',
|
|
682
|
-
'Arguments: $ARGUMENTS',
|
|
683
|
-
'',
|
|
684
|
-
'Primary tools:',
|
|
685
|
-
...skill.tools.map((tool) => `- \`${tool.name}\``),
|
|
686
|
-
...(hasRelatedResources
|
|
687
|
-
? [
|
|
688
|
-
'',
|
|
689
|
-
'Related resources:',
|
|
690
|
-
...skill.resources.map((resource) => {
|
|
691
|
-
const label = resource.name ?? resource.title ?? resource.uri
|
|
692
|
-
return `- \`${label}\``
|
|
693
|
-
}),
|
|
694
|
-
...skill.resourceTemplates.map((template) => `- \`${template.name}\``),
|
|
695
|
-
]
|
|
696
|
-
: []),
|
|
697
|
-
...(hasRelatedPrompts
|
|
698
|
-
? [
|
|
699
|
-
'',
|
|
700
|
-
'Related prompt templates:',
|
|
701
|
-
...skill.prompts.map((prompt) => `- \`${prompt.name}\``),
|
|
702
|
-
]
|
|
703
|
-
: []),
|
|
704
|
-
'',
|
|
705
|
-
'Workflow:',
|
|
706
|
-
'',
|
|
707
|
-
...workflowSteps,
|
|
708
|
-
].join('\n')
|
|
709
|
-
|
|
710
|
-
return wrapManagedMarkdown(
|
|
711
|
-
generatedContent,
|
|
712
|
-
existingContent,
|
|
713
|
-
{
|
|
714
|
-
customHeading: '## Custom Notes',
|
|
715
|
-
defaultCustomContent: DEFAULT_SKILL_CUSTOM_CONTENT,
|
|
716
|
-
},
|
|
717
|
-
)
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
export function buildInstructionsContent(input: {
|
|
721
|
-
displayName: string
|
|
722
|
-
description: string
|
|
723
|
-
source: McpServer
|
|
724
|
-
runtimeAuthMode?: McpRuntimeAuthMode
|
|
725
|
-
instructions?: string
|
|
726
|
-
skills: PlannedSkill[]
|
|
727
|
-
tools: IntrospectedMcpTool[]
|
|
728
|
-
resources: IntrospectedMcpResource[]
|
|
729
|
-
resourceTemplates: IntrospectedMcpResourceTemplate[]
|
|
730
|
-
prompts: IntrospectedMcpPrompt[]
|
|
731
|
-
userConfig?: UserConfigEntry[]
|
|
732
|
-
}): string {
|
|
733
|
-
const accessLine = describePluginAccess(input.displayName, input.source, input.runtimeAuthMode ?? 'inline')
|
|
734
|
-
const lines = [
|
|
735
|
-
`# ${input.displayName}`,
|
|
736
|
-
'',
|
|
737
|
-
input.description,
|
|
738
|
-
'',
|
|
739
|
-
accessLine,
|
|
740
|
-
'',
|
|
741
|
-
'## Workflow Guidance',
|
|
742
|
-
'',
|
|
743
|
-
]
|
|
744
|
-
|
|
745
|
-
for (const skill of input.skills) {
|
|
746
|
-
lines.push(`- ${buildInstructionSkillSummary(skill)}`)
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
lines.push(
|
|
750
|
-
'',
|
|
751
|
-
'## Tool Routing',
|
|
752
|
-
'',
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
for (const tool of input.tools) {
|
|
756
|
-
lines.push(`- \`${tool.name}\`: ${summarizeToolForInstructions(tool)}`)
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (input.resources.length > 0 || input.resourceTemplates.length > 0) {
|
|
760
|
-
lines.push('', '## Resource Surfaces', '')
|
|
761
|
-
|
|
762
|
-
for (const resource of input.resources.slice(0, 8)) {
|
|
763
|
-
const label = resource.name ?? resource.title ?? resource.uri
|
|
764
|
-
lines.push(`- \`${label}\`: ${summarizeResourceForInstructions(resource)}`)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
for (const template of input.resourceTemplates.slice(0, 8)) {
|
|
768
|
-
lines.push(`- \`${template.name}\`: ${summarizeResourceTemplateForInstructions(template)}`)
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (input.prompts.length > 0) {
|
|
773
|
-
lines.push('', '## Prompt Templates', '')
|
|
774
|
-
|
|
775
|
-
for (const prompt of input.prompts.slice(0, 8)) {
|
|
776
|
-
lines.push(`- \`${prompt.name}\`: ${summarizePromptForInstructions(prompt)}`)
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
lines.push(
|
|
781
|
-
'',
|
|
782
|
-
'## Operating Notes',
|
|
783
|
-
'',
|
|
784
|
-
'- Prefer the most specific tool that matches the user request.',
|
|
785
|
-
'- If the MCP exposes resources or prompt templates, use them as canonical context before improvising your own workflow.',
|
|
786
|
-
'- Confirm required inputs before calling a tool.',
|
|
787
|
-
'- Summarize returned data instead of dumping raw JSON unless the user asks for it.',
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
if (input.userConfig && input.userConfig.length > 0) {
|
|
791
|
-
lines.push('', '## User Config', '')
|
|
792
|
-
for (const item of input.userConfig) {
|
|
793
|
-
const descriptor = [item.type ?? 'string', item.required === false ? 'optional' : 'required']
|
|
794
|
-
.filter(Boolean)
|
|
795
|
-
.join(', ')
|
|
796
|
-
const envVar = item.envVar ? ` — env: \`${item.envVar}\`` : ''
|
|
797
|
-
lines.push(`- \`${item.key}\` (${item.title}; ${descriptor})${envVar}: ${item.description}`)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (input.instructions) {
|
|
802
|
-
const serverGuidance = summarizeServerGuidance(input.instructions)
|
|
803
|
-
if (serverGuidance.length > 0) {
|
|
804
|
-
lines.push('', '## Server Guidance', '')
|
|
805
|
-
for (const item of serverGuidance) {
|
|
806
|
-
lines.push(`- ${item}`)
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
lines.push('')
|
|
812
|
-
return lines.join('\n')
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
export function buildConfigTemplate(input: {
|
|
816
|
-
pluginName: string
|
|
817
|
-
authorName: string
|
|
818
|
-
description: string
|
|
819
|
-
displayName: string
|
|
820
|
-
serverName: string
|
|
821
|
-
source: McpServer
|
|
822
|
-
websiteUrl?: string
|
|
823
|
-
targets: TargetPlatform[]
|
|
824
|
-
userConfig?: UserConfigEntry[]
|
|
825
|
-
hooks?: Record<string, HookEntry[]>
|
|
826
|
-
scriptsPath?: string
|
|
827
|
-
runtimeAuthMode?: McpRuntimeAuthMode
|
|
828
|
-
commandsPath?: string
|
|
829
|
-
}): string {
|
|
830
|
-
const targets = input.targets.map((target) => JSON.stringify(target)).join(', ')
|
|
831
|
-
const mcpBlock = buildMcpBlock(input.serverName, input.source)
|
|
832
|
-
const shortDescription = truncate(firstSentenceOf(cleanSingleLineText(input.description)), 140)
|
|
833
|
-
const brandFields = [
|
|
834
|
-
`displayName: ${JSON.stringify(input.displayName)}`,
|
|
835
|
-
shortDescription ? `shortDescription: ${JSON.stringify(shortDescription)}` : null,
|
|
836
|
-
input.websiteUrl ? `websiteURL: ${JSON.stringify(input.websiteUrl)}` : null,
|
|
837
|
-
].filter(Boolean).join(',\n ')
|
|
838
|
-
const userConfigBlock = input.userConfig && input.userConfig.length > 0
|
|
839
|
-
? `\n userConfig: ${serializeUserConfig(input.userConfig)},\n`
|
|
840
|
-
: ''
|
|
841
|
-
const scriptsBlock = input.scriptsPath ? ` scripts: ${JSON.stringify(input.scriptsPath)},\n` : ''
|
|
842
|
-
const commandsBlock = input.commandsPath ? ` commands: ${JSON.stringify(input.commandsPath)},\n` : ''
|
|
843
|
-
const hooksBlock = input.hooks ? `\n hooks: ${serializeHooks(input.hooks)},\n` : ''
|
|
844
|
-
const platformsBlock = input.runtimeAuthMode === 'platform'
|
|
845
|
-
? `\n platforms: {\n 'claude-code': {\n mcpAuth: 'platform',\n },\n cursor: {\n mcpAuth: 'platform',\n },\n },\n`
|
|
846
|
-
: ''
|
|
847
|
-
|
|
848
|
-
return `import { definePlugin } from 'pluxx'
|
|
849
|
-
|
|
850
|
-
export default definePlugin({
|
|
851
|
-
name: ${JSON.stringify(input.pluginName)},
|
|
852
|
-
version: '0.1.0',
|
|
853
|
-
description: ${JSON.stringify(input.description)},
|
|
854
|
-
author: {
|
|
855
|
-
name: ${JSON.stringify(input.authorName)},
|
|
856
|
-
},
|
|
857
|
-
license: 'MIT',
|
|
858
|
-
|
|
859
|
-
skills: './skills/',
|
|
860
|
-
${commandsBlock}
|
|
861
|
-
instructions: './INSTRUCTIONS.md',
|
|
862
|
-
${userConfigBlock}
|
|
863
|
-
${scriptsBlock}
|
|
864
|
-
|
|
865
|
-
mcp: {
|
|
866
|
-
${mcpBlock}
|
|
867
|
-
},
|
|
868
|
-
${hooksBlock}
|
|
869
|
-
${platformsBlock}
|
|
870
|
-
|
|
871
|
-
brand: {
|
|
872
|
-
${brandFields}
|
|
873
|
-
},
|
|
874
|
-
|
|
875
|
-
targets: [${targets}],
|
|
876
|
-
})
|
|
877
|
-
`
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function buildMcpBlock(serverName: string, source: McpServer): string {
|
|
881
|
-
if (source.transport === 'stdio') {
|
|
882
|
-
const argsLine = source.args && source.args.length > 0
|
|
883
|
-
? `,\n args: ${JSON.stringify(source.args)}`
|
|
884
|
-
: ''
|
|
885
|
-
const envLine = source.env && Object.keys(source.env).length > 0
|
|
886
|
-
? `,\n env: ${JSON.stringify(source.env, null, 6).replace(/\n/g, '\n ')}`
|
|
887
|
-
: ''
|
|
888
|
-
|
|
889
|
-
return ` ${JSON.stringify(serverName)}: {
|
|
890
|
-
transport: 'stdio',
|
|
891
|
-
command: ${JSON.stringify(source.command)}${argsLine}${envLine}
|
|
892
|
-
},`
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
const authLine = source.auth && source.auth.type !== 'none'
|
|
896
|
-
? source.auth.type === 'platform'
|
|
897
|
-
? `,\n auth: {\n type: 'platform',\n mode: ${JSON.stringify(source.auth.mode ?? 'oauth')}\n }`
|
|
898
|
-
: `,\n auth: {\n type: ${JSON.stringify(source.auth.type)},\n envVar: ${JSON.stringify(source.auth.envVar)}${source.auth.type === 'header'
|
|
899
|
-
? `,\n headerName: ${JSON.stringify(source.auth.headerName)},\n headerTemplate: ${JSON.stringify(source.auth.headerTemplate)}`
|
|
900
|
-
: ''}\n }`
|
|
901
|
-
: ''
|
|
902
|
-
const transportLine = source.transport === 'sse'
|
|
903
|
-
? `\n transport: 'sse',`
|
|
904
|
-
: ''
|
|
905
|
-
|
|
906
|
-
return ` ${JSON.stringify(serverName)}: {${transportLine}
|
|
907
|
-
url: ${JSON.stringify(source.url)}${authLine}
|
|
908
|
-
},`
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function serializeHooks(hooks: Record<string, HookEntry[]>): string {
|
|
912
|
-
const entries = Object.entries(hooks)
|
|
913
|
-
.map(([event, hookEntries]) => {
|
|
914
|
-
const serializedEntries = hookEntries
|
|
915
|
-
.map((entry) => {
|
|
916
|
-
const fields = [
|
|
917
|
-
entry.type && entry.type !== 'command' ? `type: ${JSON.stringify(entry.type)}` : null,
|
|
918
|
-
entry.command ? `command: ${JSON.stringify(entry.command)}` : null,
|
|
919
|
-
entry.prompt ? `prompt: ${JSON.stringify(entry.prompt)}` : null,
|
|
920
|
-
entry.model ? `model: ${JSON.stringify(entry.model)}` : null,
|
|
921
|
-
entry.timeout !== undefined ? `timeout: ${entry.timeout}` : null,
|
|
922
|
-
entry.matcher ? `matcher: ${JSON.stringify(entry.matcher)}` : null,
|
|
923
|
-
entry.failClosed !== undefined ? `failClosed: ${entry.failClosed}` : null,
|
|
924
|
-
entry.loop_limit !== undefined ? `loop_limit: ${entry.loop_limit}` : null,
|
|
925
|
-
].filter(Boolean)
|
|
926
|
-
|
|
927
|
-
return ` {\n ${fields.join(',\n ')}\n }`
|
|
928
|
-
})
|
|
929
|
-
.join(',\n')
|
|
930
|
-
|
|
931
|
-
return ` ${event}: [\n${serializedEntries}\n ]`
|
|
932
|
-
})
|
|
933
|
-
.join(',\n')
|
|
934
|
-
|
|
935
|
-
return `{\n${entries}\n }`
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function serializeUserConfig(userConfig: UserConfigEntry[]): string {
|
|
939
|
-
const entries = userConfig
|
|
940
|
-
.map((item) => {
|
|
941
|
-
const fields = [
|
|
942
|
-
`key: ${JSON.stringify(item.key)}`,
|
|
943
|
-
`title: ${JSON.stringify(item.title)}`,
|
|
944
|
-
`description: ${JSON.stringify(item.description)}`,
|
|
945
|
-
`type: ${JSON.stringify(item.type ?? 'string')}`,
|
|
946
|
-
`required: ${item.required ?? true}`,
|
|
947
|
-
item.envVar ? `envVar: ${JSON.stringify(item.envVar)}` : null,
|
|
948
|
-
item.defaultValue !== undefined ? `defaultValue: ${JSON.stringify(item.defaultValue)}` : null,
|
|
949
|
-
item.placeholder ? `placeholder: ${JSON.stringify(item.placeholder)}` : null,
|
|
950
|
-
item.targets ? `targets: ${JSON.stringify(item.targets)}` : null,
|
|
951
|
-
].filter(Boolean)
|
|
952
|
-
|
|
953
|
-
return ` {\n ${fields.join(',\n ')}\n }`
|
|
954
|
-
})
|
|
955
|
-
.join(',\n')
|
|
956
|
-
|
|
957
|
-
return `[\n${entries}\n ]`
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const MUTATING_PREFIXES = [
|
|
961
|
-
'create', 'add', 'insert',
|
|
962
|
-
'update', 'edit', 'modify', 'patch',
|
|
963
|
-
'delete', 'remove', 'destroy', 'drop', 'purge',
|
|
964
|
-
'bulk', 'send', 'post', 'publish',
|
|
965
|
-
] as const
|
|
966
|
-
|
|
967
|
-
const MUTATING_PREFIX_PATTERN = new RegExp(`^(${MUTATING_PREFIXES.join('|')})\\b`, 'i')
|
|
968
|
-
|
|
969
|
-
export function detectMutatingTools(tools: IntrospectedMcpTool[]): string[] {
|
|
970
|
-
return tools
|
|
971
|
-
.filter((tool) => {
|
|
972
|
-
const normalized = normalizeIdentifier(tool.name).trim().toLowerCase()
|
|
973
|
-
return MUTATING_PREFIX_PATTERN.test(normalized)
|
|
974
|
-
})
|
|
975
|
-
.map((tool) => tool.name)
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function planGeneratedHooks(source: McpServer, tools: IntrospectedMcpTool[], serverName: string, hookMode: McpHookMode = 'none'): GeneratedHookScaffold {
|
|
979
|
-
if (hookMode !== 'safe') {
|
|
980
|
-
return { mode: 'none', files: [] }
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
const envVars = collectRequiredEnvVars(source)
|
|
984
|
-
const mutatingTools = detectMutatingTools(tools)
|
|
985
|
-
|
|
986
|
-
if (envVars.length === 0 && mutatingTools.length === 0) {
|
|
987
|
-
return { mode: 'none', files: [] }
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const hookEntries: Record<string, HookEntry[]> = {}
|
|
991
|
-
const files: Array<{ relativePath: string; content: string }> = []
|
|
992
|
-
|
|
993
|
-
if (envVars.length > 0) {
|
|
994
|
-
hookEntries.sessionStart = [{
|
|
995
|
-
type: 'command',
|
|
996
|
-
command: 'bash "${PLUGIN_ROOT}/scripts/check-env.sh"',
|
|
997
|
-
}]
|
|
998
|
-
files.push({
|
|
999
|
-
relativePath: 'scripts/check-env.sh',
|
|
1000
|
-
content: buildEnvValidationScript(envVars),
|
|
1001
|
-
})
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (mutatingTools.length > 0) {
|
|
1005
|
-
hookEntries.preToolUse = mutatingTools.map((toolName) => ({
|
|
1006
|
-
type: 'command',
|
|
1007
|
-
command: 'bash "${PLUGIN_ROOT}/scripts/confirm-mutation.sh"',
|
|
1008
|
-
matcher: buildMcpToolMatcher(serverName, toolName),
|
|
1009
|
-
}))
|
|
1010
|
-
files.push({
|
|
1011
|
-
relativePath: 'scripts/confirm-mutation.sh',
|
|
1012
|
-
content: buildMutationConfirmationScript(mutatingTools),
|
|
1013
|
-
})
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
return {
|
|
1017
|
-
mode: 'safe',
|
|
1018
|
-
scriptsPath: './scripts/',
|
|
1019
|
-
hookEntries,
|
|
1020
|
-
files,
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export function buildMutationConfirmationScript(mutatingTools: string[]): string {
|
|
1025
|
-
const toolList = JSON.stringify(mutatingTools.map(sanitizeShellCommentText))
|
|
1026
|
-
return `#!/usr/bin/env bash
|
|
1027
|
-
set -euo pipefail
|
|
1028
|
-
# This hook runs before mutating MCP tools.
|
|
1029
|
-
# The platform will prompt the user for confirmation.
|
|
1030
|
-
# Mutating tools: ${toolList}
|
|
1031
|
-
echo "pluxx: This tool modifies data. The agent should confirm before proceeding." >&2
|
|
1032
|
-
`
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
function collectRequiredEnvVars(source: McpServer): string[] {
|
|
1036
|
-
const envVars = new Set<string>()
|
|
1037
|
-
|
|
1038
|
-
if (source.auth?.type && source.auth.type !== 'none' && source.auth.type !== 'platform') {
|
|
1039
|
-
if (isValidShellEnvVarName(source.auth.envVar)) {
|
|
1040
|
-
envVars.add(source.auth.envVar)
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (source.transport === 'stdio') {
|
|
1045
|
-
for (const key of Object.keys(source.env ?? {})) {
|
|
1046
|
-
if (isValidShellEnvVarName(key)) {
|
|
1047
|
-
envVars.add(key)
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return [...envVars]
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function buildEnvValidationScript(envVars: string[]): string {
|
|
1056
|
-
const checks = envVars
|
|
1057
|
-
.map((envVar) => `if [ -z "\${${envVar}:-}" ]; then
|
|
1058
|
-
echo "pluxx: ${envVar} is not set. Export it before using this plugin." >&2
|
|
1059
|
-
exit 1
|
|
1060
|
-
fi`)
|
|
1061
|
-
.join('\n\n')
|
|
1062
|
-
|
|
1063
|
-
return `#!/usr/bin/env bash
|
|
1064
|
-
set -euo pipefail
|
|
1065
|
-
|
|
1066
|
-
${checks}
|
|
1067
|
-
`
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function buildMcpToolMatcher(serverName: string, toolName: string): string {
|
|
1071
|
-
return `mcp__${serverName}__${toolName}`
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function sanitizeShellCommentText(value: string): string {
|
|
1075
|
-
return value.replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, ' ').trim()
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function isValidShellEnvVarName(value: string): boolean {
|
|
1079
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function buildMcpScaffoldMetadata(input: {
|
|
1083
|
-
source: McpServer
|
|
1084
|
-
introspection: IntrospectedMcpServer
|
|
1085
|
-
pluginName: string
|
|
1086
|
-
displayName: string
|
|
1087
|
-
skillGrouping: McpSkillGrouping
|
|
1088
|
-
requestedHookMode: McpHookMode
|
|
1089
|
-
generatedHookMode: McpHookMode
|
|
1090
|
-
generatedHookEvents: string[]
|
|
1091
|
-
runtimeAuthMode: McpRuntimeAuthMode
|
|
1092
|
-
userConfig: UserConfigEntry[]
|
|
1093
|
-
plannedSkills: PlannedSkill[]
|
|
1094
|
-
managedFiles: string[]
|
|
1095
|
-
}): McpScaffoldMetadata {
|
|
1096
|
-
return {
|
|
1097
|
-
version: 1,
|
|
1098
|
-
source: input.source,
|
|
1099
|
-
serverInfo: input.introspection.serverInfo,
|
|
1100
|
-
settings: {
|
|
1101
|
-
pluginName: input.pluginName,
|
|
1102
|
-
displayName: input.displayName,
|
|
1103
|
-
description: deriveScaffoldDescription({
|
|
1104
|
-
displayName: input.displayName,
|
|
1105
|
-
introspection: input.introspection,
|
|
1106
|
-
plannedSkills: input.plannedSkills,
|
|
1107
|
-
}),
|
|
1108
|
-
skillGrouping: input.skillGrouping,
|
|
1109
|
-
requestedHookMode: input.requestedHookMode,
|
|
1110
|
-
generatedHookMode: input.generatedHookMode,
|
|
1111
|
-
generatedHookEvents: input.generatedHookEvents,
|
|
1112
|
-
runtimeAuthMode: input.runtimeAuthMode,
|
|
1113
|
-
},
|
|
1114
|
-
userConfig: input.userConfig,
|
|
1115
|
-
tools: input.introspection.tools,
|
|
1116
|
-
resources: input.introspection.resources ?? [],
|
|
1117
|
-
resourceTemplates: input.introspection.resourceTemplates ?? [],
|
|
1118
|
-
prompts: input.introspection.prompts ?? [],
|
|
1119
|
-
skills: input.plannedSkills.map((skill) => ({
|
|
1120
|
-
dirName: skill.dirName,
|
|
1121
|
-
title: skill.title,
|
|
1122
|
-
description: skill.description,
|
|
1123
|
-
toolNames: skill.tools.map((tool) => tool.name),
|
|
1124
|
-
resourceUris: skill.resources.map((resource) => resource.uri),
|
|
1125
|
-
resourceTemplateUris: skill.resourceTemplates.map((resource) => resource.uriTemplate),
|
|
1126
|
-
promptNames: skill.prompts.map((prompt) => prompt.name),
|
|
1127
|
-
})),
|
|
1128
|
-
managedFiles: input.managedFiles,
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
export function planSkillScaffolds(
|
|
1133
|
-
tools: IntrospectedMcpTool[],
|
|
1134
|
-
grouping: McpSkillGrouping = 'workflow',
|
|
1135
|
-
resources: IntrospectedMcpResource[] = [],
|
|
1136
|
-
resourceTemplates: IntrospectedMcpResourceTemplate[] = [],
|
|
1137
|
-
prompts: IntrospectedMcpPrompt[] = [],
|
|
1138
|
-
): PlannedSkill[] {
|
|
1139
|
-
if (grouping === 'tool') {
|
|
1140
|
-
return allocateSkillDirectoryNames(
|
|
1141
|
-
attachRelatedSurfaceSignals(
|
|
1142
|
-
tools
|
|
1143
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
1144
|
-
.map((tool) => ({
|
|
1145
|
-
dirName: toKebabCase(tool.name) || 'tool',
|
|
1146
|
-
title: tool.title ?? humanizeName(tool.name),
|
|
1147
|
-
description: tool.description ?? `Use the \`${tool.name}\` MCP tool for this workflow.`,
|
|
1148
|
-
tools: [tool],
|
|
1149
|
-
resources: [],
|
|
1150
|
-
resourceTemplates: [],
|
|
1151
|
-
prompts: [],
|
|
1152
|
-
workflowKey: classifyToolWorkflow(tool),
|
|
1153
|
-
})),
|
|
1154
|
-
resources,
|
|
1155
|
-
resourceTemplates,
|
|
1156
|
-
prompts,
|
|
1157
|
-
),
|
|
1158
|
-
)
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const categoryBuckets = new Map<string, IntrospectedMcpTool[]>()
|
|
1162
|
-
const standaloneTools: IntrospectedMcpTool[] = []
|
|
1163
|
-
|
|
1164
|
-
for (const tool of tools) {
|
|
1165
|
-
const category = classifyToolWorkflow(tool)
|
|
1166
|
-
if (!category) {
|
|
1167
|
-
standaloneTools.push(tool)
|
|
1168
|
-
continue
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const bucket = categoryBuckets.get(category) ?? []
|
|
1172
|
-
bucket.push(tool)
|
|
1173
|
-
categoryBuckets.set(category, bucket)
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const plannedSkills: PlannedSkill[] = []
|
|
1177
|
-
|
|
1178
|
-
for (const definition of WORKFLOW_SKILL_DEFINITIONS) {
|
|
1179
|
-
const bucket = categoryBuckets.get(definition.key)
|
|
1180
|
-
if (!bucket || bucket.length === 0) continue
|
|
1181
|
-
|
|
1182
|
-
plannedSkills.push({
|
|
1183
|
-
dirName: definition.key,
|
|
1184
|
-
title: definition.title,
|
|
1185
|
-
description: definition.description,
|
|
1186
|
-
tools: bucket.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1187
|
-
resources: [],
|
|
1188
|
-
resourceTemplates: [],
|
|
1189
|
-
prompts: [],
|
|
1190
|
-
workflowKey: definition.key,
|
|
1191
|
-
})
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
for (const tool of standaloneTools.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1195
|
-
plannedSkills.push({
|
|
1196
|
-
dirName: toKebabCase(tool.name) || 'tool',
|
|
1197
|
-
title: tool.title ?? humanizeName(tool.name),
|
|
1198
|
-
description: tool.description ?? `Use the \`${tool.name}\` MCP tool for this workflow.`,
|
|
1199
|
-
tools: [tool],
|
|
1200
|
-
resources: [],
|
|
1201
|
-
resourceTemplates: [],
|
|
1202
|
-
prompts: [],
|
|
1203
|
-
workflowKey: classifyToolWorkflow(tool),
|
|
1204
|
-
})
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
return allocateSkillDirectoryNames(
|
|
1208
|
-
attachRelatedSurfaceSignals(plannedSkills, resources, resourceTemplates, prompts),
|
|
1209
|
-
)
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function planSkillScaffoldsFromPersisted(
|
|
1213
|
-
tools: IntrospectedMcpTool[],
|
|
1214
|
-
grouping: McpSkillGrouping = 'workflow',
|
|
1215
|
-
resources: IntrospectedMcpResource[] = [],
|
|
1216
|
-
resourceTemplates: IntrospectedMcpResourceTemplate[] = [],
|
|
1217
|
-
prompts: IntrospectedMcpPrompt[] = [],
|
|
1218
|
-
persistedSkills: PersistedSkill[] = [],
|
|
1219
|
-
toolRenames: Map<string, string> = new Map(),
|
|
1220
|
-
): PlannedSkill[] {
|
|
1221
|
-
if (persistedSkills.length === 0) {
|
|
1222
|
-
return planSkillScaffolds(tools, grouping, resources, resourceTemplates, prompts)
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const toolByName = new Map(tools.map((tool) => [tool.name, tool]))
|
|
1226
|
-
const assigned = new Set<string>()
|
|
1227
|
-
const planned: PlannedSkill[] = []
|
|
1228
|
-
|
|
1229
|
-
for (const skill of persistedSkills) {
|
|
1230
|
-
const matchedTools: IntrospectedMcpTool[] = []
|
|
1231
|
-
|
|
1232
|
-
for (const originalToolName of skill.toolNames) {
|
|
1233
|
-
const resolvedToolName = toolRenames.get(originalToolName) ?? originalToolName
|
|
1234
|
-
const tool = toolByName.get(resolvedToolName)
|
|
1235
|
-
if (!tool || assigned.has(tool.name)) continue
|
|
1236
|
-
matchedTools.push(tool)
|
|
1237
|
-
assigned.add(tool.name)
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (matchedTools.length === 0) continue
|
|
1241
|
-
|
|
1242
|
-
planned.push({
|
|
1243
|
-
dirName: skill.dirName,
|
|
1244
|
-
title: skill.title,
|
|
1245
|
-
description: skill.description ?? `Handle ${skill.title.toLowerCase()} workflows.`,
|
|
1246
|
-
tools: matchedTools,
|
|
1247
|
-
resources: [],
|
|
1248
|
-
resourceTemplates: [],
|
|
1249
|
-
prompts: [],
|
|
1250
|
-
workflowKey: inferWorkflowKeyFromTools(matchedTools),
|
|
1251
|
-
})
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
const remainingTools = tools.filter((tool) => !assigned.has(tool.name))
|
|
1255
|
-
if (remainingTools.length > 0) {
|
|
1256
|
-
planned.push(...planSkillScaffolds(remainingTools, grouping, resources, resourceTemplates, prompts))
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
return allocateSkillDirectoryNames(
|
|
1260
|
-
attachRelatedSurfaceSignals(planned, resources, resourceTemplates, prompts),
|
|
1261
|
-
)
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function buildPersistedTaxonomy(skills: PlannedSkill[]): PersistedSkill[] {
|
|
1265
|
-
return skills.map((skill) => ({
|
|
1266
|
-
dirName: skill.dirName,
|
|
1267
|
-
title: skill.title,
|
|
1268
|
-
description: skill.description,
|
|
1269
|
-
toolNames: skill.tools.map((tool) => tool.name),
|
|
1270
|
-
}))
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
export function analyzeMcpQuality(
|
|
1274
|
-
tools: IntrospectedMcpTool[],
|
|
1275
|
-
plannedSkills: PlannedSkill[] = planSkillScaffolds(tools, 'workflow'),
|
|
1276
|
-
): McpQualityReport {
|
|
1277
|
-
const issues: McpQualityIssue[] = []
|
|
1278
|
-
|
|
1279
|
-
const genericNameTools = tools.filter((tool) => {
|
|
1280
|
-
const normalized = toKebabCase(tool.name)
|
|
1281
|
-
return GENERIC_TOOL_NAMES.has(normalized)
|
|
1282
|
-
})
|
|
1283
|
-
|
|
1284
|
-
if (genericNameTools.length > 0) {
|
|
1285
|
-
issues.push({
|
|
1286
|
-
level: 'warning',
|
|
1287
|
-
code: 'generic-tool-names',
|
|
1288
|
-
title: 'Generic MCP tool names',
|
|
1289
|
-
detail: `${genericNameTools.length} tool(s) use generic names: ${genericNameTools.slice(0, 5).map((tool) => tool.name).join(', ')}`,
|
|
1290
|
-
fix: 'Add sharper tool names or use Agent Mode with docs/website context to recover better taxonomy.',
|
|
1291
|
-
})
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const missingDescriptionTools = tools.filter((tool) => cleanSingleLineText(tool.description).length < 12)
|
|
1295
|
-
if (missingDescriptionTools.length > 0) {
|
|
1296
|
-
issues.push({
|
|
1297
|
-
level: 'warning',
|
|
1298
|
-
code: 'missing-tool-descriptions',
|
|
1299
|
-
title: 'Weak MCP tool descriptions',
|
|
1300
|
-
detail: `${missingDescriptionTools.length} tool(s) have missing or too-short descriptions.`,
|
|
1301
|
-
fix: 'Add clearer tool descriptions, or expect the scaffold to need more agent refinement.',
|
|
1302
|
-
})
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const verboseDescriptionTools = tools.filter((tool) => {
|
|
1306
|
-
const description = tool.description ?? ''
|
|
1307
|
-
return description.includes('\n') || /returns:|args:|usage:|example:/i.test(description) || description.length > 260
|
|
1308
|
-
})
|
|
1309
|
-
if (verboseDescriptionTools.length > 0) {
|
|
1310
|
-
issues.push({
|
|
1311
|
-
level: 'info',
|
|
1312
|
-
code: 'verbose-tool-descriptions',
|
|
1313
|
-
title: 'Tool descriptions look documentation-shaped',
|
|
1314
|
-
detail: `${verboseDescriptionTools.length} tool(s) include long or multiline help text that may need agent cleanup in skills and instructions.`,
|
|
1315
|
-
fix: 'Use autopilot or rerun agent refinement with docs/website context so the output becomes more product-shaped.',
|
|
1316
|
-
})
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
const weakSchemaTools = tools.filter((tool) => {
|
|
1320
|
-
const fields = getTopLevelSchemaFields(tool.inputSchema)
|
|
1321
|
-
return fields.length >= 2 && fields.every((field) => !field.description)
|
|
1322
|
-
})
|
|
1323
|
-
if (weakSchemaTools.length > 0) {
|
|
1324
|
-
issues.push({
|
|
1325
|
-
level: 'info',
|
|
1326
|
-
code: 'weak-input-schemas',
|
|
1327
|
-
title: 'Input schemas lack field descriptions',
|
|
1328
|
-
detail: `${weakSchemaTools.length} tool(s) define multi-field input schemas without per-field descriptions.`,
|
|
1329
|
-
fix: 'Add input field descriptions or provide docs/context so agents can infer better examples and guidance.',
|
|
1330
|
-
})
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
const workflowFallbackSkills = plannedSkills.filter((skill) => skill.tools.length === 1 && toKebabCase(skill.tools[0]?.name ?? '') === skill.dirName)
|
|
1334
|
-
if (workflowFallbackSkills.length >= 2) {
|
|
1335
|
-
issues.push({
|
|
1336
|
-
level: 'warning',
|
|
1337
|
-
code: 'workflow-fallback-skills',
|
|
1338
|
-
title: 'Workflow grouping fell back to tool-level buckets',
|
|
1339
|
-
detail: `${workflowFallbackSkills.length} generated skill(s) are still direct tool wrappers, which usually means the MCP metadata is too weak for clean workflow grouping.`,
|
|
1340
|
-
fix: 'Use docs/website context, add pluxx.agent.md hints, or improve the MCP tool metadata before publishing.',
|
|
1341
|
-
})
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
const warnings = issues.filter((issue) => issue.level === 'warning').length
|
|
1345
|
-
const infos = issues.filter((issue) => issue.level === 'info').length
|
|
1346
|
-
|
|
1347
|
-
return {
|
|
1348
|
-
ok: warnings === 0,
|
|
1349
|
-
warnings,
|
|
1350
|
-
infos,
|
|
1351
|
-
issues,
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function allocateSkillDirectoryNames(skills: PlannedSkill[]): PlannedSkill[] {
|
|
1356
|
-
const used = new Set<string>()
|
|
1357
|
-
|
|
1358
|
-
return skills.map((skill) => {
|
|
1359
|
-
const base = toKebabCase(skill.dirName) || 'skill'
|
|
1360
|
-
let candidate = base
|
|
1361
|
-
let suffix = 2
|
|
1362
|
-
|
|
1363
|
-
while (used.has(candidate)) {
|
|
1364
|
-
candidate = `${base}-${suffix}`
|
|
1365
|
-
suffix += 1
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
used.add(candidate)
|
|
1369
|
-
return {
|
|
1370
|
-
...skill,
|
|
1371
|
-
dirName: candidate,
|
|
1372
|
-
}
|
|
1373
|
-
})
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function classifyToolWorkflow(tool: IntrospectedMcpTool): string | null {
|
|
1377
|
-
return classifyWorkflowSurface(
|
|
1378
|
-
[
|
|
1379
|
-
normalizeIdentifier(tool.name).toLowerCase(),
|
|
1380
|
-
normalizeIdentifier(tool.title ?? '').toLowerCase(),
|
|
1381
|
-
].join(' '),
|
|
1382
|
-
(tool.description ?? '').toLowerCase(),
|
|
1383
|
-
)
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function classifyResourceWorkflow(resource: IntrospectedMcpResource): string | null {
|
|
1387
|
-
const primaryText = [
|
|
1388
|
-
normalizeIdentifier(resource.name ?? '').toLowerCase(),
|
|
1389
|
-
normalizeIdentifier(resource.title ?? '').toLowerCase(),
|
|
1390
|
-
normalizeIdentifier(resource.uri).toLowerCase(),
|
|
1391
|
-
].join(' ')
|
|
1392
|
-
const secondaryText = (resource.description ?? '').toLowerCase()
|
|
1393
|
-
return classifyWorkflowSurface(primaryText, secondaryText)
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
function classifyResourceTemplateWorkflow(template: IntrospectedMcpResourceTemplate): string | null {
|
|
1397
|
-
const primaryText = [
|
|
1398
|
-
normalizeIdentifier(template.name).toLowerCase(),
|
|
1399
|
-
normalizeIdentifier(template.title ?? '').toLowerCase(),
|
|
1400
|
-
normalizeIdentifier(template.uriTemplate).toLowerCase(),
|
|
1401
|
-
].join(' ')
|
|
1402
|
-
const secondaryText = (template.description ?? '').toLowerCase()
|
|
1403
|
-
return classifyWorkflowSurface(primaryText, secondaryText)
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function classifyPromptWorkflow(prompt: IntrospectedMcpPrompt): string | null {
|
|
1407
|
-
const primaryText = [
|
|
1408
|
-
normalizeIdentifier(prompt.name).toLowerCase(),
|
|
1409
|
-
...(prompt.arguments ?? []).map((argument) => normalizeIdentifier(argument.name).toLowerCase()),
|
|
1410
|
-
].join(' ')
|
|
1411
|
-
const secondaryText = (prompt.description ?? '').toLowerCase()
|
|
1412
|
-
return classifyWorkflowSurface(primaryText, secondaryText)
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function classifyWorkflowSurface(primaryText: string, secondaryText: string): string | null {
|
|
1416
|
-
let bestMatch: string | null = null
|
|
1417
|
-
let bestScore = 0
|
|
1418
|
-
|
|
1419
|
-
for (const definition of WORKFLOW_SKILL_DEFINITIONS) {
|
|
1420
|
-
let score = 0
|
|
1421
|
-
|
|
1422
|
-
for (const needle of definition.match) {
|
|
1423
|
-
if (primaryText.includes(needle)) {
|
|
1424
|
-
score += 3
|
|
1425
|
-
}
|
|
1426
|
-
if (secondaryText.includes(needle)) {
|
|
1427
|
-
score += 1
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
if (score > bestScore) {
|
|
1432
|
-
bestScore = score
|
|
1433
|
-
bestMatch = definition.key
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
return bestMatch
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
function inferWorkflowKeyFromTools(tools: IntrospectedMcpTool[]): string | null {
|
|
1441
|
-
for (const tool of tools) {
|
|
1442
|
-
const workflowKey = classifyToolWorkflow(tool)
|
|
1443
|
-
if (workflowKey) return workflowKey
|
|
1444
|
-
}
|
|
1445
|
-
return null
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function attachRelatedSurfaceSignals(
|
|
1449
|
-
skills: PlannedSkill[],
|
|
1450
|
-
resources: IntrospectedMcpResource[],
|
|
1451
|
-
resourceTemplates: IntrospectedMcpResourceTemplate[],
|
|
1452
|
-
prompts: IntrospectedMcpPrompt[],
|
|
1453
|
-
): PlannedSkill[] {
|
|
1454
|
-
const nextSkills = skills.map((skill) => ({
|
|
1455
|
-
...skill,
|
|
1456
|
-
resources: [...skill.resources],
|
|
1457
|
-
resourceTemplates: [...skill.resourceTemplates],
|
|
1458
|
-
prompts: [...skill.prompts],
|
|
1459
|
-
}))
|
|
1460
|
-
|
|
1461
|
-
const attachResource = (workflowKey: string | null, apply: (skill: PlannedSkill) => void) => {
|
|
1462
|
-
if (!workflowKey) return
|
|
1463
|
-
const target = nextSkills.find((skill) => skill.workflowKey === workflowKey)
|
|
1464
|
-
if (!target) return
|
|
1465
|
-
apply(target)
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
for (const resource of resources) {
|
|
1469
|
-
attachResource(classifyResourceWorkflow(resource), (skill) => {
|
|
1470
|
-
if (!skill.resources.some((entry) => entry.uri === resource.uri)) {
|
|
1471
|
-
skill.resources.push(resource)
|
|
1472
|
-
}
|
|
1473
|
-
})
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
for (const template of resourceTemplates) {
|
|
1477
|
-
attachResource(classifyResourceTemplateWorkflow(template), (skill) => {
|
|
1478
|
-
if (!skill.resourceTemplates.some((entry) => entry.uriTemplate === template.uriTemplate)) {
|
|
1479
|
-
skill.resourceTemplates.push(template)
|
|
1480
|
-
}
|
|
1481
|
-
})
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
for (const prompt of prompts) {
|
|
1485
|
-
attachResource(classifyPromptWorkflow(prompt), (skill) => {
|
|
1486
|
-
if (!skill.prompts.some((entry) => entry.name === prompt.name)) {
|
|
1487
|
-
skill.prompts.push(prompt)
|
|
1488
|
-
}
|
|
1489
|
-
})
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
return nextSkills
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function buildSkillFrontmatterDescription(skill: PlannedSkill): string {
|
|
1496
|
-
if (skill.tools.length === 1) {
|
|
1497
|
-
const tool = skill.tools[0]
|
|
1498
|
-
const cleanedDescription = cleanSingleLineText(tool.description)
|
|
1499
|
-
const sentence = firstSentenceOf(cleanedDescription)
|
|
1500
|
-
|
|
1501
|
-
if (sentence) {
|
|
1502
|
-
return truncate(sentence, 220)
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
return truncate(cleanSingleLineText(skill.description), 220)
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
function buildInstructionSkillSummary(skill: PlannedSkill): string {
|
|
1510
|
-
const toolNames = skill.tools.map((tool) => `\`${tool.name}\``).join(', ')
|
|
1511
|
-
const description = truncate(cleanSingleLineText(skill.description), 180)
|
|
1512
|
-
const resourceLabels = [
|
|
1513
|
-
...skill.resources.map((resource) => `\`${resource.name ?? resource.title ?? resource.uri}\``),
|
|
1514
|
-
...skill.resourceTemplates.map((template) => `\`${template.name}\``),
|
|
1515
|
-
]
|
|
1516
|
-
const promptLabels = skill.prompts.map((prompt) => `\`${prompt.name}\``)
|
|
1517
|
-
const resourceNote = resourceLabels.length > 0
|
|
1518
|
-
? ` Related resources: ${resourceLabels.join(', ')}.`
|
|
1519
|
-
: ''
|
|
1520
|
-
const promptNote = promptLabels.length > 0
|
|
1521
|
-
? ` Related prompt templates: ${promptLabels.join(', ')}.`
|
|
1522
|
-
: ''
|
|
1523
|
-
return `\`${skill.dirName}\`: ${description} Primary tools: ${toolNames}.${resourceNote}${promptNote}`
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function inferCommandArgumentHint(skill: PlannedSkill): string {
|
|
1527
|
-
const fieldHints = new Set<string>()
|
|
1528
|
-
|
|
1529
|
-
for (const tool of skill.tools) {
|
|
1530
|
-
const fields = getTopLevelSchemaFields(tool.inputSchema)
|
|
1531
|
-
const prioritizedFields = fields.filter((field) => field.required)
|
|
1532
|
-
const candidates = (prioritizedFields.length > 0 ? prioritizedFields : fields).slice(0, 2)
|
|
1533
|
-
|
|
1534
|
-
for (const field of candidates) {
|
|
1535
|
-
fieldHints.add(mapSchemaFieldToArgumentHint(field.name))
|
|
1536
|
-
if (fieldHints.size >= 2) break
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (fieldHints.size >= 2) break
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
if (fieldHints.size === 0) {
|
|
1543
|
-
return '[request]'
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return [...fieldHints].slice(0, 2).map((hint) => `[${hint}]`).join(' ')
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
function buildCommandEntryBlurb(skill: PlannedSkill): string {
|
|
1550
|
-
const intent = inferSkillIntentPhrase(skill)
|
|
1551
|
-
const resourceLabels = [
|
|
1552
|
-
...skill.resources.map((resource) => resource.name ?? resource.title ?? resource.uri),
|
|
1553
|
-
...skill.resourceTemplates.map((template) => template.name),
|
|
1554
|
-
]
|
|
1555
|
-
const promptLabels = skill.prompts.map((prompt) => prompt.name)
|
|
1556
|
-
const resourceNote = resourceLabels.length > 0
|
|
1557
|
-
? ` Check related MCP resources such as ${resourceLabels.slice(0, 2).map((label) => `\`${label}\``).join(' and ')} when they fit the request.`
|
|
1558
|
-
: ''
|
|
1559
|
-
const promptNote = promptLabels.length > 0
|
|
1560
|
-
? ` Prompt templates such as ${promptLabels.slice(0, 2).map((label) => `\`${label}\``).join(' and ')} can provide a canonical starting point.`
|
|
1561
|
-
: ''
|
|
1562
|
-
return `Use this command when the user asks to ${intent}.${resourceNote}${promptNote}`
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
function inferSkillIntentPhrase(skill: PlannedSkill): string {
|
|
1566
|
-
const toolDescription = skill.tools.length === 1
|
|
1567
|
-
? firstSentenceOf(cleanSingleLineText(skill.tools[0].description))
|
|
1568
|
-
: ''
|
|
1569
|
-
const fallback = firstSentenceOf(cleanSingleLineText(skill.description))
|
|
1570
|
-
const cleaned = normalizeSkillIntentPhrase(toolDescription || fallback)
|
|
1571
|
-
|
|
1572
|
-
if (!cleaned) {
|
|
1573
|
-
return `work with ${skill.title.toLowerCase()} in this plugin`
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
return startsWithActionVerb(cleaned) ? cleaned : `work on ${cleaned}`
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
function normalizeSkillIntentPhrase(value: string): string {
|
|
1580
|
-
if (!value) return ''
|
|
1581
|
-
|
|
1582
|
-
const withoutGenericPrefixes = value
|
|
1583
|
-
.replace(/^use (?:the |this )?(?:workflow|command|tool)\s+(?:for|to)\s+/i, '')
|
|
1584
|
-
.replace(/^use the `[^`]+` mcp tool for this workflow\.?/i, '')
|
|
1585
|
-
.replace(/^handle\s+/i, '')
|
|
1586
|
-
.replace(/^this (?:workflow|command|tool)\s+/i, '')
|
|
1587
|
-
.replace(/\s+for this plugin\.?$/i, '')
|
|
1588
|
-
.replace(/\s+for this workflow\.?$/i, '')
|
|
1589
|
-
.trim()
|
|
1590
|
-
|
|
1591
|
-
if (!withoutGenericPrefixes) return ''
|
|
1592
|
-
|
|
1593
|
-
const normalized = withoutGenericPrefixes.charAt(0).toLowerCase() + withoutGenericPrefixes.slice(1)
|
|
1594
|
-
return normalized.replace(/[.?!]+$/, '').trim()
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
function startsWithActionVerb(value: string): boolean {
|
|
1598
|
-
return /^(search|find|look up|get|fetch|create|update|delete|list|query|send|check|compare|build|run|research)\b/i.test(value)
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
function mapSchemaFieldToArgumentHint(fieldName: string): string {
|
|
1602
|
-
const value = fieldName.toLowerCase()
|
|
1603
|
-
|
|
1604
|
-
if (value.includes('query') || value.includes('search') || value.includes('keyword')) return 'query'
|
|
1605
|
-
if (value.includes('company') || value.includes('organization') || value.includes('organisation') || value.includes('account')) return 'company'
|
|
1606
|
-
if (value.includes('role') || value.includes('title')) return 'role'
|
|
1607
|
-
if (value.includes('domain')) return 'domain'
|
|
1608
|
-
if (value.includes('url')) return 'url'
|
|
1609
|
-
if (value.includes('email')) return 'email'
|
|
1610
|
-
if (value === 'id' || value.endsWith('id')) return 'id'
|
|
1611
|
-
if (value.includes('name')) return 'name'
|
|
1612
|
-
|
|
1613
|
-
return value.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'request'
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
function summarizeToolForInstructions(tool: IntrospectedMcpTool): string {
|
|
1617
|
-
const fallback = 'Use this tool when its inputs match the user request.'
|
|
1618
|
-
const description = tool.description ?? ''
|
|
1619
|
-
const base = firstSentenceOf(cleanSingleLineText(description))
|
|
1620
|
-
const bestFor = extractLabeledGuidance(description, 'Best for')
|
|
1621
|
-
const useWhen = extractLabeledGuidance(description, 'Use when')
|
|
1622
|
-
|
|
1623
|
-
const parts = [base].filter(Boolean)
|
|
1624
|
-
const guidance = bestFor || useWhen
|
|
1625
|
-
|
|
1626
|
-
if (guidance) {
|
|
1627
|
-
parts.push(`Best for: ${guidance}.`)
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
if (parts.length === 0) {
|
|
1631
|
-
return fallback
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
return truncate(parts.join(' '), 240)
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
function summarizeResourceForInstructions(resource: IntrospectedMcpResource): string {
|
|
1638
|
-
const descriptor = firstSentenceOf(cleanSingleLineText(resource.description ?? ''))
|
|
1639
|
-
const location = resource.uri ? `URI: \`${resource.uri}\`.` : ''
|
|
1640
|
-
const mimeType = resource.mimeType ? ` Format: ${resource.mimeType}.` : ''
|
|
1641
|
-
const parts = [
|
|
1642
|
-
descriptor || 'Reference resource exposed by the MCP.',
|
|
1643
|
-
location,
|
|
1644
|
-
mimeType,
|
|
1645
|
-
].filter(Boolean)
|
|
1646
|
-
|
|
1647
|
-
return truncate(parts.join(' '), 240)
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
function summarizeResourceTemplateForInstructions(template: IntrospectedMcpResourceTemplate): string {
|
|
1651
|
-
const descriptor = firstSentenceOf(cleanSingleLineText(template.description ?? ''))
|
|
1652
|
-
const uriTemplate = template.uriTemplate ? `URI template: \`${template.uriTemplate}\`.` : ''
|
|
1653
|
-
const mimeType = template.mimeType ? ` Format: ${template.mimeType}.` : ''
|
|
1654
|
-
const parts = [
|
|
1655
|
-
descriptor || 'Parameterized MCP resource template.',
|
|
1656
|
-
uriTemplate,
|
|
1657
|
-
mimeType,
|
|
1658
|
-
].filter(Boolean)
|
|
1659
|
-
|
|
1660
|
-
return truncate(parts.join(' '), 240)
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
function summarizePromptForInstructions(prompt: IntrospectedMcpPrompt): string {
|
|
1664
|
-
const descriptor = firstSentenceOf(cleanSingleLineText(prompt.description ?? ''))
|
|
1665
|
-
const args = prompt.arguments?.slice(0, 4).map((argument) => {
|
|
1666
|
-
const required = argument.required ? 'required' : 'optional'
|
|
1667
|
-
return `\`${argument.name}\` (${required})`
|
|
1668
|
-
}) ?? []
|
|
1669
|
-
const parts = [
|
|
1670
|
-
descriptor || 'Prompt template exposed by the MCP.',
|
|
1671
|
-
args.length > 0 ? `Arguments: ${args.join(', ')}.` : '',
|
|
1672
|
-
].filter(Boolean)
|
|
1673
|
-
|
|
1674
|
-
return truncate(parts.join(' '), 240)
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
function summarizeServerGuidance(value: string): string[] {
|
|
1678
|
-
const lines = value
|
|
1679
|
-
.split('\n')
|
|
1680
|
-
.map((line) => line.trim())
|
|
1681
|
-
.filter(Boolean)
|
|
1682
|
-
.filter((line) => !line.startsWith('```'))
|
|
1683
|
-
.map((line) => line.replace(/^#+\s*/, ''))
|
|
1684
|
-
.map((line) => line.replace(/^[-*]\s+/, ''))
|
|
1685
|
-
.filter((line) => !/^example:?$/i.test(line))
|
|
1686
|
-
.map((line) => truncate(cleanSingleLineText(line), 220))
|
|
1687
|
-
.filter(Boolean)
|
|
1688
|
-
|
|
1689
|
-
return [...new Set(lines)].slice(0, 4)
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function extractLabeledGuidance(value: string | undefined, label: string): string {
|
|
1693
|
-
if (!value) return ''
|
|
1694
|
-
|
|
1695
|
-
const normalizedLabel = `${label.toLowerCase()}:`
|
|
1696
|
-
for (const rawLine of value.split('\n')) {
|
|
1697
|
-
const line = rawLine
|
|
1698
|
-
.trim()
|
|
1699
|
-
.replace(/^\*+|\*+$/g, '')
|
|
1700
|
-
.replace(/^[-*]\s*/, '')
|
|
1701
|
-
.replace(/^\*+\s*/, '')
|
|
1702
|
-
.replace(/\s+\*+$/, '')
|
|
1703
|
-
if (!line) continue
|
|
1704
|
-
|
|
1705
|
-
const plain = line.replace(/[*`_]/g, '')
|
|
1706
|
-
if (plain.toLowerCase().startsWith(normalizedLabel)) {
|
|
1707
|
-
return truncate(cleanSingleLineText(plain.slice(normalizedLabel.length).trim()), 160)
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
return ''
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function getTopLevelSchemaFields(inputSchema?: Record<string, unknown>): SchemaField[] {
|
|
1715
|
-
if (!inputSchema) return []
|
|
1716
|
-
|
|
1717
|
-
const rawProperties = inputSchema.properties
|
|
1718
|
-
if (!rawProperties || typeof rawProperties !== 'object') {
|
|
1719
|
-
return []
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
const required = new Set(
|
|
1723
|
-
Array.isArray(inputSchema.required)
|
|
1724
|
-
? inputSchema.required.filter((value): value is string => typeof value === 'string')
|
|
1725
|
-
: [],
|
|
1726
|
-
)
|
|
1727
|
-
|
|
1728
|
-
return Object.entries(rawProperties).map(([name, value]) => {
|
|
1729
|
-
const schema = typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
|
|
1730
|
-
return {
|
|
1731
|
-
name,
|
|
1732
|
-
type: formatSchemaType(schema.type),
|
|
1733
|
-
required: required.has(name),
|
|
1734
|
-
description: typeof schema.description === 'string' ? schema.description : undefined,
|
|
1735
|
-
}
|
|
1736
|
-
})
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
export function buildToolExampleRequest(tool: IntrospectedMcpTool): string {
|
|
1740
|
-
const action = inferToolAction(tool)
|
|
1741
|
-
const objectLabel = inferToolObject(tool)
|
|
1742
|
-
const context = buildToolRequestContext(tool)
|
|
1743
|
-
const sentence = buildExampleSentence(action, objectLabel, context)
|
|
1744
|
-
|
|
1745
|
-
return sentence.charAt(0).toUpperCase() + sentence.slice(1)
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
function inferToolAction(tool: IntrospectedMcpTool): string {
|
|
1749
|
-
const identifier = normalizeIdentifier(tool.title ?? tool.name).toLowerCase()
|
|
1750
|
-
|
|
1751
|
-
if (/^(get|fetch|lookup|look up)\b/.test(identifier)) return 'look up'
|
|
1752
|
-
if (/^(create|add)\b/.test(identifier)) return 'create'
|
|
1753
|
-
if (/^(update|edit)\b/.test(identifier)) return 'update'
|
|
1754
|
-
if (/^(delete|remove)\b/.test(identifier)) return 'delete'
|
|
1755
|
-
if (/^(list)\b/.test(identifier)) return 'list'
|
|
1756
|
-
if (/^(query)\b/.test(identifier)) return 'query'
|
|
1757
|
-
if (/^(search)\b/.test(identifier)) return 'search'
|
|
1758
|
-
if (/^(send|post|publish)\b/.test(identifier)) return 'send'
|
|
1759
|
-
return 'find'
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function inferToolObject(tool: IntrospectedMcpTool): string {
|
|
1763
|
-
const raw = normalizeIdentifier(tool.title ?? tool.name).trim()
|
|
1764
|
-
const stripped = raw.replace(/^(find|get|fetch|lookup|look up|search|list|create|add|update|edit|delete|remove|query)\s+/i, '')
|
|
1765
|
-
const tokens = (stripped || raw).toLowerCase().split(/\s+/).filter(Boolean)
|
|
1766
|
-
|
|
1767
|
-
while (tokens.length > 1 && SECONDARY_ACTION_TOKENS.has(tokens[0])) {
|
|
1768
|
-
tokens.shift()
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
while (tokens.length > 1 && NOISE_OBJECT_TOKENS.has(tokens[tokens.length - 1])) {
|
|
1772
|
-
tokens.pop()
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const candidate = tokens.join(' ').trim()
|
|
1776
|
-
return candidate || 'results'
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
function buildToolRequestContext(tool: IntrospectedMcpTool): string {
|
|
1780
|
-
const requiredFields = getTopLevelSchemaFields(tool.inputSchema).filter((field) => field.required)
|
|
1781
|
-
const preferredField = requiredFields[0]
|
|
1782
|
-
|
|
1783
|
-
if (!preferredField) return ''
|
|
1784
|
-
|
|
1785
|
-
const placeholder = `<${preferredField.name}>`
|
|
1786
|
-
const fieldName = preferredField.name.toLowerCase()
|
|
1787
|
-
|
|
1788
|
-
if (fieldName.endsWith('id') || fieldName === 'id') {
|
|
1789
|
-
return `using ${placeholder}`
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
if (fieldName.includes('query') || fieldName.includes('keyword') || fieldName.includes('search')) {
|
|
1793
|
-
return `matching ${placeholder}`
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
if (fieldName.includes('role') || fieldName.includes('title')) {
|
|
1797
|
-
return `for ${placeholder}`
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
if (fieldName.includes('company') || fieldName.includes('organization') || fieldName.includes('organisation') || fieldName.includes('account')) {
|
|
1801
|
-
return `for ${placeholder}`
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
if (fieldName.includes('domain') || fieldName.includes('email') || fieldName.includes('url')) {
|
|
1805
|
-
return `using ${placeholder}`
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
return `with ${placeholder}`
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
const SECONDARY_ACTION_TOKENS = new Set([
|
|
1812
|
-
'find',
|
|
1813
|
-
'get',
|
|
1814
|
-
'fetch',
|
|
1815
|
-
'lookup',
|
|
1816
|
-
'search',
|
|
1817
|
-
'list',
|
|
1818
|
-
'create',
|
|
1819
|
-
'add',
|
|
1820
|
-
'update',
|
|
1821
|
-
'edit',
|
|
1822
|
-
'delete',
|
|
1823
|
-
'remove',
|
|
1824
|
-
'query',
|
|
1825
|
-
'send',
|
|
1826
|
-
'post',
|
|
1827
|
-
'publish',
|
|
1828
|
-
])
|
|
1829
|
-
|
|
1830
|
-
const NOISE_OBJECT_TOKENS = new Set(['tool', 'workflow', 'mcp'])
|
|
1831
|
-
|
|
1832
|
-
function buildExampleSentence(action: string, objectLabel: string, context: string): string {
|
|
1833
|
-
const objectPhrase = buildObjectPhrase(action, objectLabel)
|
|
1834
|
-
const withContext = context ? `${objectPhrase} ${context}` : objectPhrase
|
|
1835
|
-
return `${withContext}.`
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
function buildObjectPhrase(action: string, objectLabel: string): string {
|
|
1839
|
-
const trimmed = objectLabel.trim() || 'results'
|
|
1840
|
-
const pluralized = maybePluralizePhrase(trimmed)
|
|
1841
|
-
|
|
1842
|
-
if (action === 'create') return `create a new ${trimmed}`
|
|
1843
|
-
if (action === 'update') return `update ${withArticle(trimmed)}`
|
|
1844
|
-
if (action === 'delete') return `delete the ${trimmed}`
|
|
1845
|
-
if (action === 'look up') return `look up ${withArticle(trimmed)}`
|
|
1846
|
-
if (action === 'list') return `list ${pluralized}`
|
|
1847
|
-
if (action === 'find' || action === 'search' || action === 'query') return `${action} ${pluralized}`
|
|
1848
|
-
if (action === 'send') return `send ${withArticle(trimmed)}`
|
|
1849
|
-
return `${action} ${trimmed}`
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
function withArticle(value: string): string {
|
|
1853
|
-
if (!value) return 'results'
|
|
1854
|
-
if (/^(a|an|the)\b/i.test(value)) return value
|
|
1855
|
-
if (/\b(and|or)\b/i.test(value) || value.endsWith('s')) return value
|
|
1856
|
-
const firstWord = value.split(/\s+/)[0].toLowerCase()
|
|
1857
|
-
const article = /^[aeiou]/.test(firstWord) ? 'an' : 'a'
|
|
1858
|
-
return `${article} ${value}`
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
function maybePluralizePhrase(value: string): string {
|
|
1862
|
-
const words = value.trim().split(/\s+/).filter(Boolean)
|
|
1863
|
-
if (words.length === 0) return 'results'
|
|
1864
|
-
|
|
1865
|
-
const last = words[words.length - 1]
|
|
1866
|
-
if (/s$/i.test(last) || /people$/i.test(last)) return value
|
|
1867
|
-
if (/y$/i.test(last) && !/[aeiou]y$/i.test(last)) {
|
|
1868
|
-
words[words.length - 1] = `${last.slice(0, -1)}ies`
|
|
1869
|
-
return words.join(' ')
|
|
1870
|
-
}
|
|
1871
|
-
if (/(ch|sh|x|z)$/i.test(last)) {
|
|
1872
|
-
words[words.length - 1] = `${last}es`
|
|
1873
|
-
return words.join(' ')
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
words[words.length - 1] = `${last}s`
|
|
1877
|
-
return words.join(' ')
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
function formatSchemaType(value: unknown): string {
|
|
1881
|
-
if (typeof value === 'string') return value
|
|
1882
|
-
if (Array.isArray(value)) {
|
|
1883
|
-
const parts = value.filter((entry): entry is string => typeof entry === 'string')
|
|
1884
|
-
if (parts.length > 0) return parts.join(' | ')
|
|
1885
|
-
}
|
|
1886
|
-
return 'unknown'
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
function describePluginAccess(displayName: string, source: McpServer, runtimeAuthMode: McpRuntimeAuthMode): string {
|
|
1890
|
-
if (source.transport === 'stdio') {
|
|
1891
|
-
const command = [source.command, ...(source.args ?? [])].join(' ')
|
|
1892
|
-
const authLine = describeAuthRequirement(source)
|
|
1893
|
-
return `${displayName} connects through a local stdio MCP command (\`${command}\`).${authLine ? ` ${authLine}` : ''}`
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
const transportLabel = source.transport === 'sse' ? 'legacy SSE' : 'HTTP'
|
|
1897
|
-
const authLine = describeAuthRequirement(source, runtimeAuthMode)
|
|
1898
|
-
return `${displayName} connects to its MCP over ${transportLabel}.${authLine ? ` ${authLine}` : ''}`
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
function describeAuthRequirement(source: McpServer, runtimeAuthMode: McpRuntimeAuthMode = 'inline'): string {
|
|
1902
|
-
if (runtimeAuthMode === 'platform' && source.transport !== 'stdio') {
|
|
1903
|
-
if (source.auth?.type === 'platform') {
|
|
1904
|
-
return 'This server relies on platform-managed OAuth at runtime. Pluxx does not complete interactive OAuth in the CLI; use the host-native auth flow before calling authenticated tools.'
|
|
1905
|
-
}
|
|
1906
|
-
return 'Claude Code and Cursor use platform-managed auth at runtime (for example native OAuth/custom connector flows). Exported env vars remain useful for scaffold refreshes and other non-platform-managed targets like Codex and OpenCode.'
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
if (!source.auth || source.auth.type === 'none') {
|
|
1910
|
-
return ''
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
if (source.auth.type === 'platform') {
|
|
1914
|
-
return 'This server relies on platform-managed OAuth at runtime. Pluxx does not complete interactive OAuth in the CLI; use the host-native auth flow before calling authenticated tools.'
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
if (source.auth.type === 'header') {
|
|
1918
|
-
return `Export \`${source.auth.envVar}\` so Pluxx can send ${source.auth.headerName ?? 'the required auth header'}.`
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
return `Export \`${source.auth.envVar}\` before using authenticated tools.`
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function humanizeName(value: string): string {
|
|
1925
|
-
return normalizeIdentifier(value)
|
|
1926
|
-
.split(/\s+/)
|
|
1927
|
-
.filter(Boolean)
|
|
1928
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1929
|
-
.join(' ')
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
function toKebabCase(value: string): string {
|
|
1933
|
-
return normalizeIdentifier(value)
|
|
1934
|
-
.toLowerCase()
|
|
1935
|
-
.trim()
|
|
1936
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
1937
|
-
.replace(/^-+|-+$/g, '')
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
function normalizeIdentifier(value: string): string {
|
|
1941
|
-
return value
|
|
1942
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
1943
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
1944
|
-
.replace(/[._/]+/g, ' ')
|
|
1945
|
-
.replace(/-/g, ' ')
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
function truncate(value: string, maxLength: number): string {
|
|
1949
|
-
if (value.length <= maxLength) return value
|
|
1950
|
-
return `${value.slice(0, maxLength - 1).trimEnd()}…`
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
function cleanSingleLineText(value: string | undefined): string {
|
|
1954
|
-
if (!value) return ''
|
|
1955
|
-
|
|
1956
|
-
return value
|
|
1957
|
-
.replace(/\s+/g, ' ')
|
|
1958
|
-
.replace(/\s*[-*]\s+/g, ' ')
|
|
1959
|
-
.trim()
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
function firstSentenceOf(value: string): string {
|
|
1963
|
-
if (!value) return ''
|
|
1964
|
-
|
|
1965
|
-
const match = value.match(/^(.+?[.?!])(?:\s|$)/)
|
|
1966
|
-
return (match?.[1] ?? value).trim()
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function splitCommandString(command: string): string[] {
|
|
1970
|
-
const parts: string[] = []
|
|
1971
|
-
let current = ''
|
|
1972
|
-
let quote: '"' | "'" | null = null
|
|
1973
|
-
let escaping = false
|
|
1974
|
-
|
|
1975
|
-
for (const char of command) {
|
|
1976
|
-
if (escaping) {
|
|
1977
|
-
current += char
|
|
1978
|
-
escaping = false
|
|
1979
|
-
continue
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
if (char === '\\') {
|
|
1983
|
-
escaping = true
|
|
1984
|
-
continue
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
if (quote) {
|
|
1988
|
-
if (char === quote) {
|
|
1989
|
-
quote = null
|
|
1990
|
-
} else {
|
|
1991
|
-
current += char
|
|
1992
|
-
}
|
|
1993
|
-
continue
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
if (char === '"' || char === "'") {
|
|
1997
|
-
quote = char
|
|
1998
|
-
continue
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
if (/\s/.test(char)) {
|
|
2002
|
-
if (current) {
|
|
2003
|
-
parts.push(current)
|
|
2004
|
-
current = ''
|
|
2005
|
-
}
|
|
2006
|
-
continue
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
current += char
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
if (quote) {
|
|
2013
|
-
throw new Error('Unterminated quote in MCP command.')
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
if (escaping) {
|
|
2017
|
-
current += '\\'
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
if (current) {
|
|
2021
|
-
parts.push(current)
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
return parts
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
export function derivePluginName(introspection: IntrospectedMcpServer, source: McpServer): string {
|
|
2028
|
-
const candidates = [
|
|
2029
|
-
introspection.serverInfo.name,
|
|
2030
|
-
introspection.serverInfo.title,
|
|
2031
|
-
source.transport === 'stdio' ? basename(source.command) : new URL(source.url).hostname.split('.')[0],
|
|
2032
|
-
].filter((value): value is string => Boolean(value))
|
|
2033
|
-
|
|
2034
|
-
for (const candidate of candidates) {
|
|
2035
|
-
const normalized = toKebabCase(candidate)
|
|
2036
|
-
if (normalized) return normalized
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
return 'mcp-plugin'
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
export function deriveDisplayName(introspection: IntrospectedMcpServer, pluginName: string): string {
|
|
2043
|
-
const candidates = [
|
|
2044
|
-
introspection.serverInfo.title,
|
|
2045
|
-
introspection.serverInfo.name,
|
|
2046
|
-
pluginName,
|
|
2047
|
-
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
2048
|
-
|
|
2049
|
-
for (const candidate of candidates) {
|
|
2050
|
-
const polished = polishDisplayName(candidate)
|
|
2051
|
-
if (polished) return polished
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
return 'MCP Plugin'
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
function polishDisplayName(value: string): string {
|
|
2058
|
-
const raw = value.trim()
|
|
2059
|
-
if (!raw) return ''
|
|
2060
|
-
|
|
2061
|
-
const looksMachineIdentifier = /^[a-z0-9._/-]+$/.test(raw) || /[-_/]/.test(raw) || raw === raw.toLowerCase()
|
|
2062
|
-
let candidate = looksMachineIdentifier ? humanizeName(raw) : raw
|
|
2063
|
-
|
|
2064
|
-
candidate = candidate
|
|
2065
|
-
.replace(/\bmcp\b/gi, 'MCP')
|
|
2066
|
-
.replace(/\bMCP\s+Server\b/gi, 'MCP')
|
|
2067
|
-
.replace(/\s+/g, ' ')
|
|
2068
|
-
.trim()
|
|
2069
|
-
|
|
2070
|
-
return candidate
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
function deriveScaffoldDescription(input: {
|
|
2074
|
-
displayName: string
|
|
2075
|
-
introspection: IntrospectedMcpServer
|
|
2076
|
-
plannedSkills: PlannedSkill[]
|
|
2077
|
-
}): string {
|
|
2078
|
-
const serverDescription = truncate(cleanSingleLineText(input.introspection.serverInfo.description), 200)
|
|
2079
|
-
if (serverDescription && !isGenericServerDescription(serverDescription)) {
|
|
2080
|
-
return serverDescription
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
const skillTitles = [...new Set(
|
|
2084
|
-
input.plannedSkills
|
|
2085
|
-
.map((skill) => cleanSingleLineText(skill.title).toLowerCase())
|
|
2086
|
-
.filter(Boolean),
|
|
2087
|
-
)].slice(0, 2)
|
|
2088
|
-
|
|
2089
|
-
if (skillTitles.length === 1) {
|
|
2090
|
-
return `${input.displayName} plugin scaffold for ${skillTitles[0]} workflows.`
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
if (skillTitles.length === 2) {
|
|
2094
|
-
return `${input.displayName} plugin scaffold for ${skillTitles[0]} and ${skillTitles[1]} workflows.`
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
const toolCount = input.introspection.tools.length
|
|
2098
|
-
if (toolCount > 0) {
|
|
2099
|
-
return `${input.displayName} plugin scaffold for ${toolCount} MCP tool${toolCount === 1 ? '' : 's'}.`
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
return `${input.displayName} plugin scaffold for MCP-driven workflows.`
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
function isGenericServerDescription(value: string): boolean {
|
|
2106
|
-
const normalized = value.toLowerCase().trim()
|
|
2107
|
-
if (!normalized) return true
|
|
2108
|
-
|
|
2109
|
-
if (normalized.startsWith('generated from ')) return true
|
|
2110
|
-
if (normalized === 'mcp server') return true
|
|
2111
|
-
if (normalized === 'an mcp server') return true
|
|
2112
|
-
if (normalized === 'a mcp server') return true
|
|
2113
|
-
|
|
2114
|
-
return false
|
|
2115
|
-
}
|