@orchid-labs/pluxx 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +100 -522
  2. package/dist/cli/agent.d.ts +7 -0
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/eval.d.ts +22 -0
  7. package/dist/cli/eval.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +19 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/init-from-mcp.d.ts +17 -2
  11. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  12. package/dist/cli/install.d.ts +2 -0
  13. package/dist/cli/install.d.ts.map +1 -1
  14. package/dist/cli/lint.d.ts +5 -1
  15. package/dist/cli/lint.d.ts.map +1 -1
  16. package/dist/cli/mcp-proxy.d.ts +10 -0
  17. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  18. package/dist/cli/migrate.d.ts.map +1 -1
  19. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  20. package/dist/cli/test.d.ts +2 -0
  21. package/dist/cli/test.d.ts.map +1 -1
  22. package/dist/generators/claude-code/index.d.ts +2 -0
  23. package/dist/generators/claude-code/index.d.ts.map +1 -1
  24. package/dist/generators/codex/index.d.ts +1 -0
  25. package/dist/generators/codex/index.d.ts.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +99 -1
  29. package/dist/mcp/introspect.d.ts +43 -1
  30. package/dist/mcp/introspect.d.ts.map +1 -1
  31. package/dist/permissions.d.ts.map +1 -1
  32. package/dist/validation/platform-rules.d.ts +20 -0
  33. package/dist/validation/platform-rules.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/cli/agent.ts +459 -34
  36. package/src/cli/doctor.ts +400 -1
  37. package/src/cli/eval.ts +470 -0
  38. package/src/cli/index.ts +633 -114
  39. package/src/cli/init-from-mcp.ts +545 -41
  40. package/src/cli/install.ts +166 -4
  41. package/src/cli/lint.ts +56 -26
  42. package/src/cli/mcp-proxy.ts +322 -0
  43. package/src/cli/migrate.ts +256 -3
  44. package/src/cli/sync-from-mcp.ts +23 -0
  45. package/src/cli/test.ts +10 -2
  46. package/src/generators/claude-code/index.ts +143 -0
  47. package/src/generators/codex/index.ts +23 -0
  48. package/src/index.ts +12 -1
  49. package/src/mcp/introspect.ts +297 -24
  50. package/src/permissions.ts +3 -1
  51. package/src/validation/platform-rules.ts +121 -0
@@ -2,7 +2,13 @@ import { existsSync } from 'fs'
2
2
  import { mkdir } from 'fs/promises'
3
3
  import { basename, resolve } from 'path'
4
4
  import type { HookEntry, McpServer, PluginConfig, TargetPlatform, UserConfigEntry } from '../schema'
5
- import type { IntrospectedMcpServer, IntrospectedMcpTool } from '../mcp/introspect'
5
+ import type {
6
+ IntrospectedMcpPrompt,
7
+ IntrospectedMcpResource,
8
+ IntrospectedMcpResourceTemplate,
9
+ IntrospectedMcpServer,
10
+ IntrospectedMcpTool,
11
+ } from '../mcp/introspect'
6
12
  import { collectUserConfigEntries } from '../user-config'
7
13
 
8
14
  export interface McpScaffoldOptions {
@@ -47,6 +53,10 @@ interface PlannedSkill {
47
53
  title: string
48
54
  description: string
49
55
  tools: IntrospectedMcpTool[]
56
+ resources: IntrospectedMcpResource[]
57
+ resourceTemplates: IntrospectedMcpResourceTemplate[]
58
+ prompts: IntrospectedMcpPrompt[]
59
+ workflowKey?: string | null
50
60
  }
51
61
 
52
62
  export interface PersistedSkill {
@@ -102,6 +112,7 @@ export interface McpScaffoldMetadata {
102
112
  settings: {
103
113
  pluginName: string
104
114
  displayName: string
115
+ description?: string
105
116
  skillGrouping: McpSkillGrouping
106
117
  requestedHookMode: McpHookMode
107
118
  generatedHookMode: McpHookMode
@@ -110,11 +121,17 @@ export interface McpScaffoldMetadata {
110
121
  }
111
122
  userConfig: UserConfigEntry[]
112
123
  tools: IntrospectedMcpTool[]
124
+ resources?: IntrospectedMcpResource[]
125
+ resourceTemplates?: IntrospectedMcpResourceTemplate[]
126
+ prompts?: IntrospectedMcpPrompt[]
113
127
  skills: Array<{
114
128
  dirName: string
115
129
  title: string
116
130
  description?: string
117
131
  toolNames: string[]
132
+ resourceUris?: string[]
133
+ resourceTemplateUris?: string[]
134
+ promptNames?: string[]
118
135
  }>
119
136
  managedFiles: string[]
120
137
  }
@@ -277,25 +294,35 @@ export async function applyMcpScaffoldPlan(rootDir: string, plan: McpScaffoldPla
277
294
  export async function planMcpScaffold(options: McpScaffoldOptions): Promise<McpScaffoldPlan> {
278
295
  const pluginName = toKebabCase(options.pluginName) || 'mcp-plugin'
279
296
  const displayName = options.displayName
280
- ?? options.introspection.serverInfo.title
281
- ?? humanizeName(pluginName)
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
+ )
282
307
  const description = options.description
283
- ?? options.introspection.serverInfo.description
284
- ?? `Generated from the ${displayName} MCP server.`
308
+ ?? deriveScaffoldDescription({
309
+ displayName,
310
+ introspection: options.introspection,
311
+ plannedSkills,
312
+ })
285
313
  const serverName = options.serverName
286
314
  ?? toKebabCase(options.introspection.serverInfo.name)
287
315
  ?? pluginName
288
- const runtimeAuthMode = options.runtimeAuthMode ?? 'inline'
316
+ const runtimeAuthMode = options.runtimeAuthMode
317
+ ?? (
318
+ options.source.transport !== 'stdio' && options.source.auth?.type === 'platform'
319
+ ? 'platform'
320
+ : 'inline'
321
+ )
289
322
 
290
323
  const instructionsPath = resolve(options.rootDir, 'INSTRUCTIONS.md')
291
324
  const skillRoot = resolve(options.rootDir, 'skills')
292
325
  const commandsRoot = resolve(options.rootDir, 'commands')
293
- const plannedSkills = planSkillScaffoldsFromPersisted(
294
- options.introspection.tools,
295
- options.skillGrouping,
296
- options.persistedSkills,
297
- options.toolRenames,
298
- )
299
326
  const userConfigSource = {
300
327
  targets: options.targets,
301
328
  mcp: {
@@ -356,6 +383,9 @@ export async function planMcpScaffold(options: McpScaffoldOptions): Promise<McpS
356
383
  instructions: options.introspection.instructions,
357
384
  skills: plannedSkills,
358
385
  tools: options.introspection.tools,
386
+ resources: options.introspection.resources ?? [],
387
+ resourceTemplates: options.introspection.resourceTemplates ?? [],
388
+ prompts: options.introspection.prompts ?? [],
359
389
  userConfig,
360
390
  }),
361
391
  existsSync(instructionsPath) ? await Bun.file(instructionsPath).text() : undefined,
@@ -572,6 +602,31 @@ export function buildSkillContent(skill: PlannedSkill): string {
572
602
  }
573
603
  }
574
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
+
575
630
  lines.push(
576
631
  '## Example Requests',
577
632
  '',
@@ -597,25 +652,59 @@ export function buildSkillContent(skill: PlannedSkill): string {
597
652
  export function buildCommandContent(skill: PlannedSkill, existingContent?: string): string {
598
653
  const description = truncate(cleanSingleLineText(skill.description), 140)
599
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
+
600
674
  const generatedContent = [
601
675
  '---',
602
676
  `description: ${JSON.stringify(description)}`,
603
677
  `argument-hint: ${JSON.stringify(argumentHint)}`,
604
678
  '---',
605
679
  '',
606
- `Use the ${skill.title.toLowerCase()} workflow for this plugin.`,
680
+ entryBlurb,
607
681
  '',
608
682
  'Arguments: $ARGUMENTS',
609
683
  '',
610
684
  'Primary tools:',
611
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
+ : []),
612
704
  '',
613
705
  'Workflow:',
614
706
  '',
615
- '1. Interpret `$ARGUMENTS` as the user request for this workflow.',
616
- '2. Choose the most specific tool in this surface.',
617
- '3. Ask for missing required inputs only if the request does not already provide them.',
618
- '4. Return a concise task-focused answer instead of raw JSON unless the user asks for it.',
707
+ ...workflowSteps,
619
708
  ].join('\n')
620
709
 
621
710
  return wrapManagedMarkdown(
@@ -636,6 +725,9 @@ export function buildInstructionsContent(input: {
636
725
  instructions?: string
637
726
  skills: PlannedSkill[]
638
727
  tools: IntrospectedMcpTool[]
728
+ resources: IntrospectedMcpResource[]
729
+ resourceTemplates: IntrospectedMcpResourceTemplate[]
730
+ prompts: IntrospectedMcpPrompt[]
639
731
  userConfig?: UserConfigEntry[]
640
732
  }): string {
641
733
  const accessLine = describePluginAccess(input.displayName, input.source, input.runtimeAuthMode ?? 'inline')
@@ -664,11 +756,33 @@ export function buildInstructionsContent(input: {
664
756
  lines.push(`- \`${tool.name}\`: ${summarizeToolForInstructions(tool)}`)
665
757
  }
666
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
+
667
780
  lines.push(
668
781
  '',
669
782
  '## Operating Notes',
670
783
  '',
671
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.',
672
786
  '- Confirm required inputs before calling a tool.',
673
787
  '- Summarize returned data instead of dumping raw JSON unless the user asks for it.',
674
788
  )
@@ -715,8 +829,10 @@ export function buildConfigTemplate(input: {
715
829
  }): string {
716
830
  const targets = input.targets.map((target) => JSON.stringify(target)).join(', ')
717
831
  const mcpBlock = buildMcpBlock(input.serverName, input.source)
832
+ const shortDescription = truncate(firstSentenceOf(cleanSingleLineText(input.description)), 140)
718
833
  const brandFields = [
719
834
  `displayName: ${JSON.stringify(input.displayName)}`,
835
+ shortDescription ? `shortDescription: ${JSON.stringify(shortDescription)}` : null,
720
836
  input.websiteUrl ? `websiteURL: ${JSON.stringify(input.websiteUrl)}` : null,
721
837
  ].filter(Boolean).join(',\n ')
722
838
  const userConfigBlock = input.userConfig && input.userConfig.length > 0
@@ -984,6 +1100,11 @@ function buildMcpScaffoldMetadata(input: {
984
1100
  settings: {
985
1101
  pluginName: input.pluginName,
986
1102
  displayName: input.displayName,
1103
+ description: deriveScaffoldDescription({
1104
+ displayName: input.displayName,
1105
+ introspection: input.introspection,
1106
+ plannedSkills: input.plannedSkills,
1107
+ }),
987
1108
  skillGrouping: input.skillGrouping,
988
1109
  requestedHookMode: input.requestedHookMode,
989
1110
  generatedHookMode: input.generatedHookMode,
@@ -992,11 +1113,17 @@ function buildMcpScaffoldMetadata(input: {
992
1113
  },
993
1114
  userConfig: input.userConfig,
994
1115
  tools: input.introspection.tools,
1116
+ resources: input.introspection.resources ?? [],
1117
+ resourceTemplates: input.introspection.resourceTemplates ?? [],
1118
+ prompts: input.introspection.prompts ?? [],
995
1119
  skills: input.plannedSkills.map((skill) => ({
996
1120
  dirName: skill.dirName,
997
1121
  title: skill.title,
998
1122
  description: skill.description,
999
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),
1000
1127
  })),
1001
1128
  managedFiles: input.managedFiles,
1002
1129
  }
@@ -1005,17 +1132,29 @@ function buildMcpScaffoldMetadata(input: {
1005
1132
  export function planSkillScaffolds(
1006
1133
  tools: IntrospectedMcpTool[],
1007
1134
  grouping: McpSkillGrouping = 'workflow',
1135
+ resources: IntrospectedMcpResource[] = [],
1136
+ resourceTemplates: IntrospectedMcpResourceTemplate[] = [],
1137
+ prompts: IntrospectedMcpPrompt[] = [],
1008
1138
  ): PlannedSkill[] {
1009
1139
  if (grouping === 'tool') {
1010
1140
  return allocateSkillDirectoryNames(
1011
- tools
1012
- .sort((a, b) => a.name.localeCompare(b.name))
1013
- .map((tool) => ({
1014
- dirName: toKebabCase(tool.name) || 'tool',
1015
- title: tool.title ?? humanizeName(tool.name),
1016
- description: tool.description ?? `Use the \`${tool.name}\` MCP tool for this workflow.`,
1017
- tools: [tool],
1018
- })),
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
+ ),
1019
1158
  )
1020
1159
  }
1021
1160
 
@@ -1045,6 +1184,10 @@ export function planSkillScaffolds(
1045
1184
  title: definition.title,
1046
1185
  description: definition.description,
1047
1186
  tools: bucket.sort((a, b) => a.name.localeCompare(b.name)),
1187
+ resources: [],
1188
+ resourceTemplates: [],
1189
+ prompts: [],
1190
+ workflowKey: definition.key,
1048
1191
  })
1049
1192
  }
1050
1193
 
@@ -1054,20 +1197,29 @@ export function planSkillScaffolds(
1054
1197
  title: tool.title ?? humanizeName(tool.name),
1055
1198
  description: tool.description ?? `Use the \`${tool.name}\` MCP tool for this workflow.`,
1056
1199
  tools: [tool],
1200
+ resources: [],
1201
+ resourceTemplates: [],
1202
+ prompts: [],
1203
+ workflowKey: classifyToolWorkflow(tool),
1057
1204
  })
1058
1205
  }
1059
1206
 
1060
- return allocateSkillDirectoryNames(plannedSkills)
1207
+ return allocateSkillDirectoryNames(
1208
+ attachRelatedSurfaceSignals(plannedSkills, resources, resourceTemplates, prompts),
1209
+ )
1061
1210
  }
1062
1211
 
1063
1212
  function planSkillScaffoldsFromPersisted(
1064
1213
  tools: IntrospectedMcpTool[],
1065
1214
  grouping: McpSkillGrouping = 'workflow',
1215
+ resources: IntrospectedMcpResource[] = [],
1216
+ resourceTemplates: IntrospectedMcpResourceTemplate[] = [],
1217
+ prompts: IntrospectedMcpPrompt[] = [],
1066
1218
  persistedSkills: PersistedSkill[] = [],
1067
1219
  toolRenames: Map<string, string> = new Map(),
1068
1220
  ): PlannedSkill[] {
1069
1221
  if (persistedSkills.length === 0) {
1070
- return planSkillScaffolds(tools, grouping)
1222
+ return planSkillScaffolds(tools, grouping, resources, resourceTemplates, prompts)
1071
1223
  }
1072
1224
 
1073
1225
  const toolByName = new Map(tools.map((tool) => [tool.name, tool]))
@@ -1092,15 +1244,21 @@ function planSkillScaffoldsFromPersisted(
1092
1244
  title: skill.title,
1093
1245
  description: skill.description ?? `Handle ${skill.title.toLowerCase()} workflows.`,
1094
1246
  tools: matchedTools,
1247
+ resources: [],
1248
+ resourceTemplates: [],
1249
+ prompts: [],
1250
+ workflowKey: inferWorkflowKeyFromTools(matchedTools),
1095
1251
  })
1096
1252
  }
1097
1253
 
1098
1254
  const remainingTools = tools.filter((tool) => !assigned.has(tool.name))
1099
1255
  if (remainingTools.length > 0) {
1100
- planned.push(...planSkillScaffolds(remainingTools, grouping))
1256
+ planned.push(...planSkillScaffolds(remainingTools, grouping, resources, resourceTemplates, prompts))
1101
1257
  }
1102
1258
 
1103
- return allocateSkillDirectoryNames(planned)
1259
+ return allocateSkillDirectoryNames(
1260
+ attachRelatedSurfaceSignals(planned, resources, resourceTemplates, prompts),
1261
+ )
1104
1262
  }
1105
1263
 
1106
1264
  function buildPersistedTaxonomy(skills: PlannedSkill[]): PersistedSkill[] {
@@ -1216,12 +1374,45 @@ function allocateSkillDirectoryNames(skills: PlannedSkill[]): PlannedSkill[] {
1216
1374
  }
1217
1375
 
1218
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 {
1219
1387
  const primaryText = [
1220
- normalizeIdentifier(tool.name).toLowerCase(),
1221
- normalizeIdentifier(tool.title ?? '').toLowerCase(),
1388
+ normalizeIdentifier(resource.name ?? '').toLowerCase(),
1389
+ normalizeIdentifier(resource.title ?? '').toLowerCase(),
1390
+ normalizeIdentifier(resource.uri).toLowerCase(),
1222
1391
  ].join(' ')
1223
- const secondaryText = (tool.description ?? '').toLowerCase()
1392
+ const secondaryText = (resource.description ?? '').toLowerCase()
1393
+ return classifyWorkflowSurface(primaryText, secondaryText)
1394
+ }
1224
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 {
1225
1416
  let bestMatch: string | null = null
1226
1417
  let bestScore = 0
1227
1418
 
@@ -1246,6 +1437,61 @@ function classifyToolWorkflow(tool: IntrospectedMcpTool): string | null {
1246
1437
  return bestMatch
1247
1438
  }
1248
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
+
1249
1495
  function buildSkillFrontmatterDescription(skill: PlannedSkill): string {
1250
1496
  if (skill.tools.length === 1) {
1251
1497
  const tool = skill.tools[0]
@@ -1263,7 +1509,18 @@ function buildSkillFrontmatterDescription(skill: PlannedSkill): string {
1263
1509
  function buildInstructionSkillSummary(skill: PlannedSkill): string {
1264
1510
  const toolNames = skill.tools.map((tool) => `\`${tool.name}\``).join(', ')
1265
1511
  const description = truncate(cleanSingleLineText(skill.description), 180)
1266
- return `\`${skill.dirName}\`: ${description} Primary tools: ${toolNames}.`
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}`
1267
1524
  }
1268
1525
 
1269
1526
  function inferCommandArgumentHint(skill: PlannedSkill): string {
@@ -1289,6 +1546,58 @@ function inferCommandArgumentHint(skill: PlannedSkill): string {
1289
1546
  return [...fieldHints].slice(0, 2).map((hint) => `[${hint}]`).join(' ')
1290
1547
  }
1291
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
+
1292
1601
  function mapSchemaFieldToArgumentHint(fieldName: string): string {
1293
1602
  const value = fieldName.toLowerCase()
1294
1603
 
@@ -1325,6 +1634,46 @@ function summarizeToolForInstructions(tool: IntrospectedMcpTool): string {
1325
1634
  return truncate(parts.join(' '), 240)
1326
1635
  }
1327
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
+
1328
1677
  function summarizeServerGuidance(value: string): string[] {
1329
1678
  const lines = value
1330
1679
  .split('\n')
@@ -1391,10 +1740,7 @@ export function buildToolExampleRequest(tool: IntrospectedMcpTool): string {
1391
1740
  const action = inferToolAction(tool)
1392
1741
  const objectLabel = inferToolObject(tool)
1393
1742
  const context = buildToolRequestContext(tool)
1394
-
1395
- const sentence = context
1396
- ? `${action} ${objectLabel} ${context}.`
1397
- : `${action} ${objectLabel}.`
1743
+ const sentence = buildExampleSentence(action, objectLabel, context)
1398
1744
 
1399
1745
  return sentence.charAt(0).toUpperCase() + sentence.slice(1)
1400
1746
  }
@@ -1409,14 +1755,25 @@ function inferToolAction(tool: IntrospectedMcpTool): string {
1409
1755
  if (/^(list)\b/.test(identifier)) return 'list'
1410
1756
  if (/^(query)\b/.test(identifier)) return 'query'
1411
1757
  if (/^(search)\b/.test(identifier)) return 'search'
1758
+ if (/^(send|post|publish)\b/.test(identifier)) return 'send'
1412
1759
  return 'find'
1413
1760
  }
1414
1761
 
1415
1762
  function inferToolObject(tool: IntrospectedMcpTool): string {
1416
1763
  const raw = normalizeIdentifier(tool.title ?? tool.name).trim()
1417
1764
  const stripped = raw.replace(/^(find|get|fetch|lookup|look up|search|list|create|add|update|edit|delete|remove|query)\s+/i, '')
1418
- const candidate = stripped || raw
1419
- return candidate ? candidate.toLowerCase() : 'results'
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'
1420
1777
  }
1421
1778
 
1422
1779
  function buildToolRequestContext(tool: IntrospectedMcpTool): string {
@@ -1451,6 +1808,75 @@ function buildToolRequestContext(tool: IntrospectedMcpTool): string {
1451
1808
  return `with ${placeholder}`
1452
1809
  }
1453
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
+
1454
1880
  function formatSchemaType(value: unknown): string {
1455
1881
  if (typeof value === 'string') return value
1456
1882
  if (Array.isArray(value)) {
@@ -1474,7 +1900,10 @@ function describePluginAccess(displayName: string, source: McpServer, runtimeAut
1474
1900
 
1475
1901
  function describeAuthRequirement(source: McpServer, runtimeAuthMode: McpRuntimeAuthMode = 'inline'): string {
1476
1902
  if (runtimeAuthMode === 'platform' && source.transport !== 'stdio') {
1477
- 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.'
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.'
1478
1907
  }
1479
1908
 
1480
1909
  if (!source.auth || source.auth.type === 'none') {
@@ -1482,7 +1911,7 @@ function describeAuthRequirement(source: McpServer, runtimeAuthMode: McpRuntimeA
1482
1911
  }
1483
1912
 
1484
1913
  if (source.auth.type === 'platform') {
1485
- return 'Use the platform-managed auth flow before calling authenticated tools.'
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.'
1486
1915
  }
1487
1916
 
1488
1917
  if (source.auth.type === 'header') {
@@ -1609,3 +2038,78 @@ export function derivePluginName(introspection: IntrospectedMcpServer, source: M
1609
2038
 
1610
2039
  return 'mcp-plugin'
1611
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
+ }