@mcp-elements/angular 0.1.0
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/LICENSE +21 -0
- package/package.json +22 -0
- package/src/accordion.component.ts +74 -0
- package/src/ai-badge.component.ts +26 -0
- package/src/alert.component.ts +25 -0
- package/src/avatar.component.ts +24 -0
- package/src/badge.component.ts +17 -0
- package/src/button.component.ts +27 -0
- package/src/card.component.ts +46 -0
- package/src/chat-bubble.component.ts +53 -0
- package/src/chips.component.ts +33 -0
- package/src/counter.component.ts +48 -0
- package/src/dialog.component.ts +42 -0
- package/src/drawer.component.ts +48 -0
- package/src/dropdown-menu.component.ts +62 -0
- package/src/feedback.component.ts +71 -0
- package/src/index.ts +86 -0
- package/src/input.component.ts +46 -0
- package/src/loader.component.ts +12 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/mcp-app-frame.component.ts +60 -0
- package/src/mcp/mcp-consent-dialog.component.ts +63 -0
- package/src/mcp/mcp-resource-browser.component.ts +86 -0
- package/src/mcp/mcp-scope-inspector.component.ts +81 -0
- package/src/mcp/mcp-server-status.component.ts +44 -0
- package/src/mcp/mcp-tool-call.component.ts +105 -0
- package/src/mcp/mcp-tool-form.component.ts +127 -0
- package/src/password-input.component.ts +35 -0
- package/src/popover.component.ts +40 -0
- package/src/progress.component.ts +20 -0
- package/src/prompt-input.component.ts +70 -0
- package/src/select.component.ts +106 -0
- package/src/separator.component.ts +15 -0
- package/src/skeleton.component.ts +11 -0
- package/src/source-card.component.ts +34 -0
- package/src/streaming-text.component.ts +43 -0
- package/src/suggestion-chips.component.ts +23 -0
- package/src/switch.component.ts +32 -0
- package/src/tabs.component.ts +95 -0
- package/src/textarea.component.ts +22 -0
- package/src/toast.component.ts +62 -0
- package/src/tooltip.directive.ts +63 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// CSS-only components
|
|
2
|
+
export { SnxButtonComponent } from './button.component'
|
|
3
|
+
export { SnxBadgeComponent } from './badge.component'
|
|
4
|
+
export {
|
|
5
|
+
SnxCardComponent,
|
|
6
|
+
SnxCardHeaderComponent,
|
|
7
|
+
SnxCardTitleComponent,
|
|
8
|
+
SnxCardDescriptionComponent,
|
|
9
|
+
SnxCardContentComponent,
|
|
10
|
+
SnxCardFooterComponent,
|
|
11
|
+
} from './card.component'
|
|
12
|
+
export { SnxInputComponent } from './input.component'
|
|
13
|
+
export { SnxTextareaComponent } from './textarea.component'
|
|
14
|
+
export { SnxAvatarComponent } from './avatar.component'
|
|
15
|
+
export { SnxSeparatorComponent } from './separator.component'
|
|
16
|
+
export { SnxSkeletonComponent } from './skeleton.component'
|
|
17
|
+
|
|
18
|
+
// Interactive components
|
|
19
|
+
export { SnxDialogComponent } from './dialog.component'
|
|
20
|
+
export {
|
|
21
|
+
SnxTabsComponent,
|
|
22
|
+
SnxTabsListComponent,
|
|
23
|
+
SnxTabsTriggerComponent,
|
|
24
|
+
SnxTabsContentComponent,
|
|
25
|
+
} from './tabs.component'
|
|
26
|
+
export {
|
|
27
|
+
SnxAccordionComponent,
|
|
28
|
+
SnxAccordionItemComponent,
|
|
29
|
+
SnxAccordionTriggerComponent,
|
|
30
|
+
SnxAccordionContentComponent,
|
|
31
|
+
} from './accordion.component'
|
|
32
|
+
export { SnxSelectComponent } from './select.component'
|
|
33
|
+
export { SnxTooltipDirective } from './tooltip.directive'
|
|
34
|
+
export { SnxPopoverComponent } from './popover.component'
|
|
35
|
+
|
|
36
|
+
// New components
|
|
37
|
+
export { SnxToasterComponent, SnxToastService } from './toast.component'
|
|
38
|
+
export { SnxDrawerComponent, SnxDrawerHeaderComponent, SnxDrawerFooterComponent, SnxDrawerTitleComponent, SnxDrawerDescriptionComponent, SnxDrawerBodyComponent } from './drawer.component'
|
|
39
|
+
export { SnxDropdownMenuComponent } from './dropdown-menu.component'
|
|
40
|
+
export { SnxSwitchComponent } from './switch.component'
|
|
41
|
+
export { SnxProgressComponent } from './progress.component'
|
|
42
|
+
export { SnxLoaderComponent } from './loader.component'
|
|
43
|
+
export { SnxChipComponent, SnxChipsComponent } from './chips.component'
|
|
44
|
+
export { SnxPasswordInputComponent } from './password-input.component'
|
|
45
|
+
export { SnxCounterComponent } from './counter.component'
|
|
46
|
+
export { SnxAlertComponent, SnxAlertTitleComponent, SnxAlertDescriptionComponent } from './alert.component'
|
|
47
|
+
|
|
48
|
+
// AI components
|
|
49
|
+
export {
|
|
50
|
+
SnxPromptInputComponent,
|
|
51
|
+
SnxPromptInputTextareaComponent,
|
|
52
|
+
SnxPromptInputFooterComponent,
|
|
53
|
+
SnxPromptInputActionsComponent,
|
|
54
|
+
SnxPromptInputCharCountComponent,
|
|
55
|
+
SnxPromptInputAttachmentsComponent,
|
|
56
|
+
SnxPromptInputAttachmentComponent,
|
|
57
|
+
} from './prompt-input.component'
|
|
58
|
+
export {
|
|
59
|
+
SnxChatBubbleComponent,
|
|
60
|
+
SnxChatBubbleAvatarComponent,
|
|
61
|
+
SnxChatBubbleContentComponent,
|
|
62
|
+
SnxChatBubbleTimestampComponent,
|
|
63
|
+
SnxChatBubbleTypingComponent,
|
|
64
|
+
} from './chat-bubble.component'
|
|
65
|
+
export { SnxAiBadgeComponent } from './ai-badge.component'
|
|
66
|
+
export { SnxSuggestionChipsComponent, SnxSuggestionChipComponent } from './suggestion-chips.component'
|
|
67
|
+
export { SnxSourceCardsComponent, SnxSourceCardComponent } from './source-card.component'
|
|
68
|
+
export {
|
|
69
|
+
SnxStreamingTextComponent,
|
|
70
|
+
SnxStreamingTextFadeInComponent,
|
|
71
|
+
SnxStreamingTextWordComponent,
|
|
72
|
+
SnxStreamingTextLineComponent,
|
|
73
|
+
SnxStreamingTextSkeletonComponent,
|
|
74
|
+
SnxStreamingTextSkeletonLineComponent,
|
|
75
|
+
} from './streaming-text.component'
|
|
76
|
+
export {
|
|
77
|
+
SnxFeedbackComponent,
|
|
78
|
+
SnxFeedbackButtonComponent,
|
|
79
|
+
SnxFeedbackSeparatorComponent,
|
|
80
|
+
SnxFeedbackFormComponent,
|
|
81
|
+
SnxFeedbackInputComponent,
|
|
82
|
+
SnxFeedbackSubmitComponent,
|
|
83
|
+
} from './feedback.component'
|
|
84
|
+
|
|
85
|
+
// MCP components
|
|
86
|
+
export * from './mcp'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Component, input, computed, output, forwardRef } from '@angular/core'
|
|
2
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'mcpe-input',
|
|
6
|
+
standalone: true,
|
|
7
|
+
template: `
|
|
8
|
+
<input
|
|
9
|
+
[class]="classes()"
|
|
10
|
+
[type]="type()"
|
|
11
|
+
[placeholder]="placeholder()"
|
|
12
|
+
[disabled]="disabled()"
|
|
13
|
+
[value]="value()"
|
|
14
|
+
(input)="onInput($event)"
|
|
15
|
+
(blur)="onTouched()"
|
|
16
|
+
/>
|
|
17
|
+
`,
|
|
18
|
+
providers: [
|
|
19
|
+
{
|
|
20
|
+
provide: NG_VALUE_ACCESSOR,
|
|
21
|
+
useExisting: forwardRef(() => SnxInputComponent),
|
|
22
|
+
multi: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
export class SnxInputComponent implements ControlValueAccessor {
|
|
27
|
+
type = input<string>('text')
|
|
28
|
+
placeholder = input('')
|
|
29
|
+
disabled = input(false)
|
|
30
|
+
value = input('')
|
|
31
|
+
class = input('')
|
|
32
|
+
|
|
33
|
+
classes = computed(() => ['mcpe-input', this.class()].filter(Boolean).join(' '))
|
|
34
|
+
|
|
35
|
+
private onChange: (value: string) => void = () => {}
|
|
36
|
+
onTouched: () => void = () => {}
|
|
37
|
+
|
|
38
|
+
onInput(event: Event) {
|
|
39
|
+
const value = (event.target as HTMLInputElement).value
|
|
40
|
+
this.onChange(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
writeValue(value: string): void {}
|
|
44
|
+
registerOnChange(fn: (value: string) => void): void { this.onChange = fn }
|
|
45
|
+
registerOnTouched(fn: () => void): void { this.onTouched = fn }
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Component, input, computed } from '@angular/core'
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'mcpe-loader',
|
|
5
|
+
standalone: true,
|
|
6
|
+
template: `<div [class]="classes()" role="status" aria-label="Loading"><span class="sr-only">Loading...</span></div>`,
|
|
7
|
+
})
|
|
8
|
+
export class SnxLoaderComponent {
|
|
9
|
+
size = input<'sm' | 'md' | 'lg' | 'xl'>('md')
|
|
10
|
+
variant = input<'primary' | 'muted'>('primary')
|
|
11
|
+
classes = computed(() => `mcpe-loader mcpe-loader-${this.size()} mcpe-loader-${this.variant()}`)
|
|
12
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { McpeMcpServerStatusComponent } from './mcp-server-status.component'
|
|
2
|
+
export type { McpConnectionStatus } from './mcp-server-status.component'
|
|
3
|
+
export { McpeMcpToolCallComponent } from './mcp-tool-call.component'
|
|
4
|
+
export { McpeMcpToolFormComponent } from './mcp-tool-form.component'
|
|
5
|
+
export { McpeMcpConsentDialogComponent } from './mcp-consent-dialog.component'
|
|
6
|
+
export { McpeMcpScopeInspectorComponent } from './mcp-scope-inspector.component'
|
|
7
|
+
export { McpeMcpResourceBrowserComponent } from './mcp-resource-browser.component'
|
|
8
|
+
export type { McpResource } from './mcp-resource-browser.component'
|
|
9
|
+
export { McpeMcpAppFrameComponent } from './mcp-app-frame.component'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component, input, output, effect, computed, ElementRef, viewChild, OnDestroy } from '@angular/core'
|
|
2
|
+
import { cn, createAppBridge } from '@mcp-elements/core'
|
|
3
|
+
import type { AppMessageEnvelope } from '@mcp-elements/core'
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'mcpe-mcp-app-frame',
|
|
7
|
+
standalone: true,
|
|
8
|
+
template: `
|
|
9
|
+
<div [class]="classes()">
|
|
10
|
+
<iframe
|
|
11
|
+
#frame
|
|
12
|
+
[src]="src()"
|
|
13
|
+
[sandbox]="sandbox()"
|
|
14
|
+
[style.height.px]="height()"
|
|
15
|
+
title="MCP App"
|
|
16
|
+
aria-label="MCP App frame"
|
|
17
|
+
style="display:block;width:100%;border:none"
|
|
18
|
+
></iframe>
|
|
19
|
+
</div>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
export class McpeMcpAppFrameComponent implements OnDestroy {
|
|
23
|
+
src = input.required<string>()
|
|
24
|
+
height = input(480)
|
|
25
|
+
sandbox = input('allow-scripts allow-same-origin')
|
|
26
|
+
class = input('')
|
|
27
|
+
onMessage = output<AppMessageEnvelope>()
|
|
28
|
+
|
|
29
|
+
frame = viewChild<ElementRef<HTMLIFrameElement>>('frame')
|
|
30
|
+
|
|
31
|
+
private _unsub: (() => void) | undefined
|
|
32
|
+
private _removeListener: (() => void) | undefined
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
effect(() => {
|
|
36
|
+
this._cleanup()
|
|
37
|
+
const bridge = createAppBridge({
|
|
38
|
+
postMessage: (env: AppMessageEnvelope) => {
|
|
39
|
+
this.frame()?.nativeElement?.contentWindow?.postMessage(env, '*')
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
const unsub = bridge.onMessage((env: AppMessageEnvelope) => this.onMessage.emit(env))
|
|
43
|
+
const handler = (e: MessageEvent) => bridge.receive(e.data)
|
|
44
|
+
window.addEventListener('message', handler)
|
|
45
|
+
this._unsub = unsub
|
|
46
|
+
this._removeListener = () => window.removeEventListener('message', handler)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _cleanup() {
|
|
51
|
+
this._unsub?.()
|
|
52
|
+
this._removeListener?.()
|
|
53
|
+
this._unsub = undefined
|
|
54
|
+
this._removeListener = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ngOnDestroy() { this._cleanup() }
|
|
58
|
+
|
|
59
|
+
classes = computed(() => cn('mcpe-mcp-app-frame', this.class()))
|
|
60
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Component, input, output, computed } from '@angular/core'
|
|
2
|
+
import { CommonModule } from '@angular/common'
|
|
3
|
+
import { parseScopes } from '@mcp-elements/core'
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'mcpe-mcp-consent-dialog',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
template: `
|
|
10
|
+
@if (open()) {
|
|
11
|
+
<div class="mcpe-dialog-overlay" (click)="deny()"></div>
|
|
12
|
+
<div class="mcpe-dialog-content" role="dialog" aria-modal="true" [attr.aria-label]="'Allow ' + serverName() + '?'">
|
|
13
|
+
<!-- Server info -->
|
|
14
|
+
<div class="mcpe-mcp-consent-dialog-server">
|
|
15
|
+
<div class="mcpe-mcp-consent-dialog-icon" aria-hidden="true">
|
|
16
|
+
@if (serverIcon()) {
|
|
17
|
+
<img [src]="serverIcon()" alt="" class="h-full w-full object-cover" />
|
|
18
|
+
} @else {
|
|
19
|
+
{{ serverName()[0]?.toUpperCase() ?? '?' }}
|
|
20
|
+
}
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<p class="mcpe-mcp-consent-dialog-server-name">{{ serverName() }}</p>
|
|
24
|
+
<p class="mcpe-mcp-consent-dialog-server-meta">is requesting access to</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<!-- Scopes -->
|
|
28
|
+
<div class="mcpe-mcp-consent-dialog-scopes" role="list" aria-label="Requested permissions">
|
|
29
|
+
@for (s of parsedScopes(); track s.raw) {
|
|
30
|
+
<div class="mcpe-mcp-consent-dialog-scope-item" role="listitem">
|
|
31
|
+
<div class="flex-1 min-w-0">
|
|
32
|
+
<p class="mcpe-mcp-consent-dialog-scope-resource">{{ s.resource }}</p>
|
|
33
|
+
<div class="mcpe-mcp-consent-dialog-scope-perms">
|
|
34
|
+
@for (p of s.permissions; track p) {
|
|
35
|
+
<span class="mcpe-mcp-consent-dialog-scope-perm">{{ p }}</span>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
}
|
|
41
|
+
</div>
|
|
42
|
+
<!-- Actions -->
|
|
43
|
+
<div class="mcpe-mcp-consent-dialog-actions">
|
|
44
|
+
<button class="mcpe-btn mcpe-btn-outline flex-1" (click)="deny()">Deny</button>
|
|
45
|
+
<button class="mcpe-btn mcpe-btn-primary flex-1" (click)="approve()">Allow</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
`,
|
|
50
|
+
})
|
|
51
|
+
export class McpeMcpConsentDialogComponent {
|
|
52
|
+
open = input.required<boolean>()
|
|
53
|
+
serverName = input.required<string>()
|
|
54
|
+
serverIcon = input<string>()
|
|
55
|
+
scopes = input<string[]>([])
|
|
56
|
+
onApprove = output<void>()
|
|
57
|
+
onDeny = output<void>()
|
|
58
|
+
|
|
59
|
+
parsedScopes = computed(() => parseScopes(this.scopes().join(' ')))
|
|
60
|
+
|
|
61
|
+
approve() { this.onApprove.emit() }
|
|
62
|
+
deny() { this.onDeny.emit() }
|
|
63
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Component, input, output, computed } from '@angular/core'
|
|
2
|
+
import { CommonModule } from '@angular/common'
|
|
3
|
+
import { cn } from '@mcp-elements/core'
|
|
4
|
+
|
|
5
|
+
export interface McpResource {
|
|
6
|
+
uri: string
|
|
7
|
+
name: string
|
|
8
|
+
mimeType?: string
|
|
9
|
+
description?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function mimeTypeLabel(mimeType?: string): string {
|
|
13
|
+
if (!mimeType) return 'res'
|
|
14
|
+
if (mimeType.includes('json')) return 'json'
|
|
15
|
+
if (mimeType.includes('text')) return 'txt'
|
|
16
|
+
if (mimeType.includes('image')) return 'img'
|
|
17
|
+
if (mimeType.includes('pdf')) return 'pdf'
|
|
18
|
+
return mimeType.split('/')[1]?.slice(0, 4) ?? 'res'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'mcpe-mcp-resource-browser',
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [CommonModule],
|
|
25
|
+
template: `
|
|
26
|
+
@if (loading()) {
|
|
27
|
+
<div [class]="classes()">
|
|
28
|
+
@for (n of skeletonItems; track n) {
|
|
29
|
+
<div class="flex items-center gap-3 px-3 py-2.5">
|
|
30
|
+
<div class="h-8 w-8 rounded-md animate-pulse bg-muted"></div>
|
|
31
|
+
<div class="h-4 flex-1 rounded animate-pulse bg-muted"></div>
|
|
32
|
+
</div>
|
|
33
|
+
}
|
|
34
|
+
</div>
|
|
35
|
+
} @else if (resources().length === 0) {
|
|
36
|
+
<div [class]="classes()">
|
|
37
|
+
<p class="mcpe-mcp-resource-browser-empty">No resources available</p>
|
|
38
|
+
</div>
|
|
39
|
+
} @else {
|
|
40
|
+
<div [class]="classes()" role="list">
|
|
41
|
+
@for (r of resources(); track r.uri) {
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
role="listitem"
|
|
45
|
+
[class]="itemClass(r.uri)"
|
|
46
|
+
[attr.aria-selected]="selectedUri() === r.uri"
|
|
47
|
+
[attr.aria-label]="r.name"
|
|
48
|
+
(click)="select(r)"
|
|
49
|
+
>
|
|
50
|
+
<span class="mcpe-mcp-resource-browser-icon" aria-hidden="true">{{ mimeLabel(r.mimeType) }}</span>
|
|
51
|
+
<span class="mcpe-mcp-resource-browser-name">{{ r.name }}</span>
|
|
52
|
+
@if (r.mimeType) {
|
|
53
|
+
<span class="mcpe-mcp-resource-browser-type">{{ r.mimeType.split('/')[0] }}</span>
|
|
54
|
+
}
|
|
55
|
+
</button>
|
|
56
|
+
}
|
|
57
|
+
</div>
|
|
58
|
+
}
|
|
59
|
+
`,
|
|
60
|
+
})
|
|
61
|
+
export class McpeMcpResourceBrowserComponent {
|
|
62
|
+
resources = input<McpResource[]>([])
|
|
63
|
+
selectedUri = input<string>()
|
|
64
|
+
loading = input(false)
|
|
65
|
+
class = input('')
|
|
66
|
+
onSelect = output<McpResource>()
|
|
67
|
+
|
|
68
|
+
readonly skeletonItems = [1, 2, 3, 4]
|
|
69
|
+
|
|
70
|
+
classes = computed(() => cn('mcpe-mcp-resource-browser', this.class()))
|
|
71
|
+
|
|
72
|
+
itemClass(uri: string): string {
|
|
73
|
+
return cn(
|
|
74
|
+
'mcpe-mcp-resource-browser-item w-full text-left',
|
|
75
|
+
this.selectedUri() === uri ? 'mcpe-mcp-resource-browser-item-selected' : ''
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mimeLabel(mimeType?: string): string {
|
|
80
|
+
return mimeTypeLabel(mimeType)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
select(r: McpResource) {
|
|
84
|
+
this.onSelect.emit(r)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Component, input, signal, computed } from '@angular/core'
|
|
2
|
+
import { CommonModule } from '@angular/common'
|
|
3
|
+
import { cn, parseScopes } from '@mcp-elements/core'
|
|
4
|
+
import type { ScopeDescriptor } from '@mcp-elements/core'
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'mcpe-mcp-scope-inspector',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [CommonModule],
|
|
10
|
+
template: `
|
|
11
|
+
<div [class]="classes()" role="list">
|
|
12
|
+
@for (s of parsedScopes(); track s.raw) {
|
|
13
|
+
<div class="mcpe-mcp-scope-inspector-item" role="listitem">
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="mcpe-mcp-scope-inspector-trigger"
|
|
17
|
+
[attr.aria-expanded]="isOpen(s.raw)"
|
|
18
|
+
(click)="toggle(s.raw)"
|
|
19
|
+
>
|
|
20
|
+
<div class="flex items-center gap-3">
|
|
21
|
+
<span class="mcpe-mcp-scope-inspector-resource">{{ s.resource }}</span>
|
|
22
|
+
<div class="mcpe-mcp-scope-inspector-perms">
|
|
23
|
+
@for (p of s.permissions; track p) {
|
|
24
|
+
<span class="mcpe-mcp-scope-inspector-perm">{{ p }}</span>
|
|
25
|
+
}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<svg
|
|
29
|
+
[class]="chevronClass(s.raw)"
|
|
30
|
+
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
31
|
+
fill="none" stroke="currentColor" stroke-width="2"
|
|
32
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
33
|
+
<path d="m6 9 6 6 6-6"/>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
@if (isOpen(s.raw) && getDescription(s)) {
|
|
37
|
+
<div role="region" class="mcpe-mcp-scope-inspector-body">
|
|
38
|
+
{{ getDescription(s) }}
|
|
39
|
+
</div>
|
|
40
|
+
}
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
`,
|
|
45
|
+
})
|
|
46
|
+
export class McpeMcpScopeInspectorComponent {
|
|
47
|
+
scopes = input<string | ScopeDescriptor[]>('')
|
|
48
|
+
descriptions = input<Record<string, string>>({})
|
|
49
|
+
class = input('')
|
|
50
|
+
|
|
51
|
+
openKeys = signal<Set<string>>(new Set())
|
|
52
|
+
|
|
53
|
+
parsedScopes = computed((): ScopeDescriptor[] => {
|
|
54
|
+
const s = this.scopes()
|
|
55
|
+
return typeof s === 'string' ? parseScopes(s) : s
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
classes = computed(() => cn('mcpe-mcp-scope-inspector', this.class()))
|
|
59
|
+
|
|
60
|
+
toggle(key: string) {
|
|
61
|
+
this.openKeys.update((prev: Set<string>) => {
|
|
62
|
+
const next = new Set(prev)
|
|
63
|
+
if (next.has(key)) next.delete(key)
|
|
64
|
+
else next.add(key)
|
|
65
|
+
return next
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isOpen(key: string): boolean {
|
|
70
|
+
return this.openKeys().has(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
chevronClass(key: string): string {
|
|
74
|
+
return cn('mcpe-mcp-scope-inspector-chevron', this.isOpen(key) ? 'mcpe-mcp-scope-inspector-chevron-open' : '')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getDescription(s: ScopeDescriptor): string | undefined {
|
|
78
|
+
const d = this.descriptions()
|
|
79
|
+
return d[s.raw] ?? d[s.resource] ?? s.description
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component, input, computed } from '@angular/core'
|
|
2
|
+
import { cn } from '@mcp-elements/core'
|
|
3
|
+
|
|
4
|
+
export type McpConnectionStatus = 'connected' | 'connecting' | 'disconnected' | 'error'
|
|
5
|
+
|
|
6
|
+
const STATUS_LABELS: Record<McpConnectionStatus, string> = {
|
|
7
|
+
connected: 'Connected',
|
|
8
|
+
connecting: 'Connecting',
|
|
9
|
+
disconnected: 'Disconnected',
|
|
10
|
+
error: 'Error',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'mcpe-mcp-server-status',
|
|
15
|
+
standalone: true,
|
|
16
|
+
template: `
|
|
17
|
+
<span
|
|
18
|
+
[class]="classes()"
|
|
19
|
+
role="status"
|
|
20
|
+
aria-live="polite"
|
|
21
|
+
[attr.aria-label]="ariaLabel()"
|
|
22
|
+
>
|
|
23
|
+
<span class="mcpe-mcp-server-status-dot" aria-hidden="true"></span>
|
|
24
|
+
{{ label() }}
|
|
25
|
+
</span>
|
|
26
|
+
`,
|
|
27
|
+
})
|
|
28
|
+
export class McpeMcpServerStatusComponent {
|
|
29
|
+
status = input.required<McpConnectionStatus>()
|
|
30
|
+
serverName = input<string>()
|
|
31
|
+
class = input('')
|
|
32
|
+
|
|
33
|
+
classes = computed(() => cn('mcpe-mcp-server-status', `mcpe-mcp-server-status-${this.status()}`, this.class()))
|
|
34
|
+
label = computed(() => {
|
|
35
|
+
const s = this.serverName()
|
|
36
|
+
const statusLabel = STATUS_LABELS[this.status() as McpConnectionStatus]
|
|
37
|
+
return s ? `${s} · ${statusLabel}` : statusLabel
|
|
38
|
+
})
|
|
39
|
+
ariaLabel = computed(() => {
|
|
40
|
+
const s = this.serverName()
|
|
41
|
+
const statusLabel = STATUS_LABELS[this.status() as McpConnectionStatus]
|
|
42
|
+
return s ? `${s}: ${statusLabel}` : statusLabel
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Component, input, output, signal, effect, computed, OnDestroy } from '@angular/core'
|
|
2
|
+
import { CommonModule } from '@angular/common'
|
|
3
|
+
import { cn } from '@mcp-elements/core'
|
|
4
|
+
import type { ToolStateApi, ToolStateSnapshot } from '@mcp-elements/core'
|
|
5
|
+
|
|
6
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
7
|
+
idle: 'idle',
|
|
8
|
+
pending: 'pending',
|
|
9
|
+
running: 'running',
|
|
10
|
+
done: 'done',
|
|
11
|
+
error: 'error',
|
|
12
|
+
cancelled: 'cancelled',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'mcpe-mcp-tool-call',
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [CommonModule],
|
|
19
|
+
template: `
|
|
20
|
+
<div [class]="classes()">
|
|
21
|
+
<!-- Header -->
|
|
22
|
+
<div class="mcpe-mcp-tool-call-header">
|
|
23
|
+
<div class="mcpe-mcp-tool-call-name">
|
|
24
|
+
<span class="mcpe-mcp-tool-call-icon" aria-hidden="true">fn</span>
|
|
25
|
+
<span class="mcpe-mcp-tool-call-title">{{ displayName() }}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<span [class]="badgeClass()">
|
|
28
|
+
@if (snap().status === 'running') {
|
|
29
|
+
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
30
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
31
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
32
|
+
</svg>
|
|
33
|
+
}
|
|
34
|
+
{{ statusLabels[snap().status] }}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
<!-- Args -->
|
|
38
|
+
@if (displayArgs()) {
|
|
39
|
+
<pre class="mcpe-mcp-tool-call-args">{{ displayArgs() | json }}</pre>
|
|
40
|
+
}
|
|
41
|
+
<!-- Progress bar -->
|
|
42
|
+
@if (snap().status === 'running') {
|
|
43
|
+
<div class="mcpe-mcp-tool-call-progress" role="progressbar" aria-label="Tool running">
|
|
44
|
+
<div class="mcpe-mcp-tool-call-progress-bar" style="width: 60%"></div>
|
|
45
|
+
</div>
|
|
46
|
+
}
|
|
47
|
+
<!-- Result -->
|
|
48
|
+
@if (snap().status === 'done' && snap().result) {
|
|
49
|
+
<div class="mcpe-mcp-tool-call-result mcpe-mcp-tool-call-result-done">
|
|
50
|
+
@for (block of textBlocks(); track $index) {
|
|
51
|
+
<p class="whitespace-pre-wrap text-sm">{{ block }}</p>
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
55
|
+
<!-- Error -->
|
|
56
|
+
@if (snap().status === 'error' && snap().error) {
|
|
57
|
+
<div class="mcpe-mcp-tool-call-result mcpe-mcp-tool-call-result-error">
|
|
58
|
+
<p class="text-sm">{{ snap().error?.message }}</p>
|
|
59
|
+
<button (click)="onRetry.emit()" class="text-xs underline underline-offset-2">Retry</button>
|
|
60
|
+
</div>
|
|
61
|
+
}
|
|
62
|
+
</div>
|
|
63
|
+
`,
|
|
64
|
+
})
|
|
65
|
+
export class McpeMcpToolCallComponent implements OnDestroy {
|
|
66
|
+
state = input.required<ToolStateApi>()
|
|
67
|
+
toolName = input<string>()
|
|
68
|
+
args = input<Record<string, unknown>>()
|
|
69
|
+
onRetry = output<void>()
|
|
70
|
+
class = input('')
|
|
71
|
+
|
|
72
|
+
readonly statusLabels = STATUS_LABELS
|
|
73
|
+
|
|
74
|
+
snap = signal<ToolStateSnapshot>({ status: 'idle' })
|
|
75
|
+
private _unsub: (() => void) | undefined
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
effect(() => {
|
|
79
|
+
this._unsub?.()
|
|
80
|
+
const s = this.state()
|
|
81
|
+
this.snap.set({
|
|
82
|
+
status: s.status,
|
|
83
|
+
tool: s.tool,
|
|
84
|
+
args: s.args,
|
|
85
|
+
result: s.result,
|
|
86
|
+
error: s.error,
|
|
87
|
+
startedAt: s.startedAt,
|
|
88
|
+
endedAt: s.endedAt,
|
|
89
|
+
})
|
|
90
|
+
this._unsub = s.subscribe((snapshot: ToolStateSnapshot) => this.snap.set({ ...snapshot }))
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ngOnDestroy() { this._unsub?.() }
|
|
95
|
+
|
|
96
|
+
classes = computed(() => cn('mcpe-mcp-tool-call', this.class()))
|
|
97
|
+
badgeClass = computed(() => cn('mcpe-mcp-tool-call-badge', `mcpe-mcp-tool-call-badge-${this.snap().status}`))
|
|
98
|
+
displayName = computed(() => this.snap().tool ?? this.toolName() ?? 'unknown')
|
|
99
|
+
displayArgs = computed(() => this.snap().args ?? this.args())
|
|
100
|
+
textBlocks = computed(() =>
|
|
101
|
+
this.snap().result?.content
|
|
102
|
+
.filter((c: { type: string }) => c.type === 'text')
|
|
103
|
+
.map((c: { type: string; text?: string }) => (c as { type: 'text'; text: string }).text) ?? []
|
|
104
|
+
)
|
|
105
|
+
}
|