@orchid-labs/pluxx 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -522
- package/dist/cli/agent.d.ts +7 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/index.d.ts +19 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-from-mcp.d.ts +17 -2
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +5 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts +10 -0
- package/dist/cli/mcp-proxy.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/test.d.ts +2 -0
- package/dist/cli/test.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts +2 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +1 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -1
- package/dist/mcp/introspect.d.ts +43 -1
- package/dist/mcp/introspect.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/validation/platform-rules.d.ts +20 -0
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/cli/agent.ts +459 -34
- package/src/cli/doctor.ts +400 -1
- package/src/cli/eval.ts +470 -0
- package/src/cli/index.ts +633 -114
- package/src/cli/init-from-mcp.ts +545 -41
- package/src/cli/install.ts +166 -4
- package/src/cli/lint.ts +56 -26
- package/src/cli/mcp-proxy.ts +322 -0
- package/src/cli/migrate.ts +256 -3
- package/src/cli/sync-from-mcp.ts +23 -0
- package/src/cli/test.ts +10 -2
- package/src/generators/claude-code/index.ts +143 -0
- package/src/generators/codex/index.ts +23 -0
- package/src/index.ts +12 -1
- package/src/mcp/introspect.ts +297 -24
- package/src/permissions.ts +3 -1
- package/src/validation/platform-rules.ts +121 -0
package/src/cli/init-from-mcp.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
281
|
-
|
|
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
|
-
??
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1221
|
-
normalizeIdentifier(
|
|
1388
|
+
normalizeIdentifier(resource.name ?? '').toLowerCase(),
|
|
1389
|
+
normalizeIdentifier(resource.title ?? '').toLowerCase(),
|
|
1390
|
+
normalizeIdentifier(resource.uri).toLowerCase(),
|
|
1222
1391
|
].join(' ')
|
|
1223
|
-
const secondaryText = (
|
|
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
|
-
|
|
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
|
|
1419
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
+
}
|