feat(frontend): add wildcard pattern support to MCP token custom scope (#7306)

* feat(mcp): add wildcard pattern support to token UI custom scope

- Add text input fields for wildcard patterns in Custom scope
- Combine wildcard patterns with individual script/flow selections
- Support comma-separated patterns (e.g., f/outline/*,f/docs/*)
- Add help popover explaining pattern syntax with examples
- Backward compatible: empty patterns preserve existing behavior

Closes #7252

* fix(mcp): apply critical code review fixes for wildcard patterns

Apply fixes identified by code-smells agent:

**P0 - Code Duplication (CRITICAL)**
- Extract pattern parsing logic into reusable helper function
- Eliminates duplicate code between scripts and flows processing
- Improves maintainability and consistency

**P1 - Button Validation (CRITICAL)**
- Fix button disable condition to allow pattern-only tokens
- Users can now create tokens with ONLY wildcard patterns
- Resolves Test Scenario #6 from design document

**P2 - State Management (MODERATE)**
- Add $effect to clear patterns when switching scopes
- Prevents stale data from persisting across mode changes
- Improves user experience and data consistency

Changes:
- Added parsePatterns() helper function
- Updated button disable condition with pattern checks
- Added two $effect hooks for state cleanup
- Reduced code duplication by 10 lines

Testing: All edge cases now properly handled including pattern-only tokens

* nits

* nit

---------

Co-authored-by: Devdatta Talele <devtalele0@gmail.com>
This commit is contained in:
hugocasa
2025-12-05 19:14:03 +01:00
committed by GitHub
parent c90d6c2557
commit 0e445f14ae
@@ -22,6 +22,7 @@
import TextInput from '../text_input/TextInput.svelte'
import Select from '../select/Select.svelte'
import { mcpEndpointTools } from '$lib/mcpEndpointTools'
import InfoIcon from 'lucide-svelte/icons/info'
interface Props {
showMcpMode?: boolean
@@ -70,6 +71,10 @@
let customScopes = $state<string[]>([])
let showCustomScopes = $state(false)
// Wildcard pattern inputs for custom scope
let customScriptPatterns = $state<string>('')
let customFlowPatterns = $state<string>('')
function ensureCurrentWorkspaceIncluded(
workspacesList: UserWorkspace[],
currentWorkspace: string | undefined
@@ -84,6 +89,21 @@
return [{ id: currentWorkspace, name: currentWorkspace }, ...workspacesList]
}
function parsePatterns(input: string): string[] {
return input
.split(',')
.map((p) => p.trim())
.filter((p) => p.length > 0)
}
// Clear pattern inputs when MCP mode is disabled OR when not in custom scope
$effect(() => {
if (!mcpCreationMode || newMcpScope !== 'custom') {
customScriptPatterns = ''
customFlowPatterns = ''
}
})
async function createToken(mcpMode: boolean = false): Promise<void> {
try {
let date: Date | undefined
@@ -94,14 +114,28 @@
let tokenScopes = scopes
if (mcpMode) {
if (newMcpScope === 'custom') {
// Granular scope format
// Granular scope format - combine individual selections with wildcard patterns
tokenScopes = []
if (selectedScripts.length > 0) {
tokenScopes.push(`mcp:scripts:${selectedScripts.join(',')}`)
// Scripts: combine individual selections with patterns
let scriptPaths = [...selectedScripts]
if (customScriptPatterns.trim()) {
scriptPaths.push(...parsePatterns(customScriptPatterns))
}
if (selectedFlows.length > 0) {
tokenScopes.push(`mcp:flows:${selectedFlows.join(',')}`)
if (scriptPaths.length > 0) {
tokenScopes.push(`mcp:scripts:${scriptPaths.join(',')}`)
}
// Flows: combine individual selections with patterns
let flowPaths = [...selectedFlows]
if (customFlowPatterns.trim()) {
flowPaths.push(...parsePatterns(customFlowPatterns))
}
if (flowPaths.length > 0) {
tokenScopes.push(`mcp:flows:${flowPaths.join(',')}`)
}
// Endpoints: no wildcard support needed
if (selectedEndpoints.length > 0) {
tokenScopes.push(`mcp:endpoints:${selectedEndpoints.join(',')}`)
}
@@ -537,6 +571,47 @@
Selected: {selectedScripts.length} scripts, {selectedFlows.length} flows, {selectedEndpoints.length}
endpoints
</div>
<!-- Wildcard Patterns Section -->
<div class="flex flex-col gap-2 mt-4 pt-4 border-t border-surface-hover">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="block text-xs font-semibold">Script wildcard patterns</span>
<Popover notClickable>
{#snippet text()}
<div class="text-xs max-w-xs">
<p class="font-semibold mb-2">Add folder wildcards or complex patterns</p>
<p class="mb-1"><b>Examples:</b></p>
<ul class="list-disc ml-4 space-y-1">
<li><code>f/folder/*</code> - all scripts/flows in folder</li>
<li><code>f/folder1/*,f/folder2/*</code> - multiple folders</li>
<li>Mix: <code>f/folder/*,f/specific/path</code></li>
</ul>
<p class="mt-2 text-xs text-secondary">
Patterns are combined with individual selections above.
</p>
</div>
{/snippet}
<Button color="light" size="xs2" nonCaptureEvent startIcon={{ icon: InfoIcon }}>
Pattern Help
</Button>
</Popover>
</div>
<TextInput
inputProps={{ placeholder: 'e.g., f/outline/*,f/docs/*' }}
bind:value={customScriptPatterns}
/>
</div>
<div class="flex flex-col gap-2 mt-2">
<div class="flex items-center justify-between">
<span class="block text-xs font-semibold">Flow wildcard patterns</span>
</div>
<TextInput
inputProps={{ placeholder: 'e.g., f/workflows/*' }}
bind:value={customFlowPatterns}
/>
</div>
</div>
</div>
{/if}
{:else if mcpCreationMode && (newMcpScope !== 'folder' || selectedFolder.length > 0)}
@@ -616,7 +691,9 @@
(newMcpScope === 'custom' &&
selectedScripts.length === 0 &&
selectedFlows.length === 0 &&
selectedEndpoints.length === 0))}
selectedEndpoints.length === 0 &&
!customScriptPatterns.trim() &&
!customFlowPatterns.trim()))}
variant="accent"
>
New token