@wener/mcps 1.0.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/LICENSE +21 -0
- package/dist/index.mjs +15 -0
- package/dist/mcps-cli.mjs +174727 -0
- package/lib/chat/agent.js +187 -0
- package/lib/chat/agent.js.map +1 -0
- package/lib/chat/audit.js +238 -0
- package/lib/chat/audit.js.map +1 -0
- package/lib/chat/converters.js +467 -0
- package/lib/chat/converters.js.map +1 -0
- package/lib/chat/handler.js +1068 -0
- package/lib/chat/handler.js.map +1 -0
- package/lib/chat/index.js +12 -0
- package/lib/chat/index.js.map +1 -0
- package/lib/chat/types.js +35 -0
- package/lib/chat/types.js.map +1 -0
- package/lib/contracts/AuditContract.js +85 -0
- package/lib/contracts/AuditContract.js.map +1 -0
- package/lib/contracts/McpsContract.js +113 -0
- package/lib/contracts/McpsContract.js.map +1 -0
- package/lib/contracts/index.js +3 -0
- package/lib/contracts/index.js.map +1 -0
- package/lib/dev.server.js +7 -0
- package/lib/dev.server.js.map +1 -0
- package/lib/entities/ChatRequestEntity.js +318 -0
- package/lib/entities/ChatRequestEntity.js.map +1 -0
- package/lib/entities/McpRequestEntity.js +271 -0
- package/lib/entities/McpRequestEntity.js.map +1 -0
- package/lib/entities/RequestLogEntity.js +177 -0
- package/lib/entities/RequestLogEntity.js.map +1 -0
- package/lib/entities/ResponseEntity.js +150 -0
- package/lib/entities/ResponseEntity.js.map +1 -0
- package/lib/entities/index.js +11 -0
- package/lib/entities/index.js.map +1 -0
- package/lib/entities/types.js +11 -0
- package/lib/entities/types.js.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/mcps-cli.js +44 -0
- package/lib/mcps-cli.js.map +1 -0
- package/lib/providers/McpServerHandlerDef.js +40 -0
- package/lib/providers/McpServerHandlerDef.js.map +1 -0
- package/lib/providers/findMcpServerDef.js +26 -0
- package/lib/providers/findMcpServerDef.js.map +1 -0
- package/lib/providers/prometheus/def.js +24 -0
- package/lib/providers/prometheus/def.js.map +1 -0
- package/lib/providers/prometheus/index.js +2 -0
- package/lib/providers/prometheus/index.js.map +1 -0
- package/lib/providers/relay/def.js +32 -0
- package/lib/providers/relay/def.js.map +1 -0
- package/lib/providers/relay/index.js +2 -0
- package/lib/providers/relay/index.js.map +1 -0
- package/lib/providers/sql/def.js +31 -0
- package/lib/providers/sql/def.js.map +1 -0
- package/lib/providers/sql/index.js +2 -0
- package/lib/providers/sql/index.js.map +1 -0
- package/lib/providers/tencent-cls/def.js +44 -0
- package/lib/providers/tencent-cls/def.js.map +1 -0
- package/lib/providers/tencent-cls/index.js +2 -0
- package/lib/providers/tencent-cls/index.js.map +1 -0
- package/lib/scripts/bundle.js +90 -0
- package/lib/scripts/bundle.js.map +1 -0
- package/lib/server/api-routes.js +96 -0
- package/lib/server/api-routes.js.map +1 -0
- package/lib/server/audit.js +274 -0
- package/lib/server/audit.js.map +1 -0
- package/lib/server/chat-routes.js +82 -0
- package/lib/server/chat-routes.js.map +1 -0
- package/lib/server/config.js +223 -0
- package/lib/server/config.js.map +1 -0
- package/lib/server/db.js +97 -0
- package/lib/server/db.js.map +1 -0
- package/lib/server/index.js +2 -0
- package/lib/server/index.js.map +1 -0
- package/lib/server/mcp-handler.js +167 -0
- package/lib/server/mcp-handler.js.map +1 -0
- package/lib/server/mcp-routes.js +112 -0
- package/lib/server/mcp-routes.js.map +1 -0
- package/lib/server/mcps-router.js +119 -0
- package/lib/server/mcps-router.js.map +1 -0
- package/lib/server/schema.js +129 -0
- package/lib/server/schema.js.map +1 -0
- package/lib/server/server.js +166 -0
- package/lib/server/server.js.map +1 -0
- package/lib/web/ChatPage.js +827 -0
- package/lib/web/ChatPage.js.map +1 -0
- package/lib/web/McpInspectorPage.js +214 -0
- package/lib/web/McpInspectorPage.js.map +1 -0
- package/lib/web/ServersPage.js +93 -0
- package/lib/web/ServersPage.js.map +1 -0
- package/lib/web/main.js +541 -0
- package/lib/web/main.js.map +1 -0
- package/package.json +83 -0
- package/src/chat/agent.ts +240 -0
- package/src/chat/audit.ts +377 -0
- package/src/chat/converters.test.ts +325 -0
- package/src/chat/converters.ts +459 -0
- package/src/chat/handler.test.ts +137 -0
- package/src/chat/handler.ts +1233 -0
- package/src/chat/index.ts +16 -0
- package/src/chat/types.ts +72 -0
- package/src/contracts/AuditContract.ts +93 -0
- package/src/contracts/McpsContract.ts +141 -0
- package/src/contracts/index.ts +18 -0
- package/src/dev.server.ts +7 -0
- package/src/entities/ChatRequestEntity.ts +157 -0
- package/src/entities/McpRequestEntity.ts +149 -0
- package/src/entities/RequestLogEntity.ts +78 -0
- package/src/entities/ResponseEntity.ts +75 -0
- package/src/entities/index.ts +12 -0
- package/src/entities/types.ts +188 -0
- package/src/index.ts +1 -0
- package/src/mcps-cli.ts +59 -0
- package/src/providers/McpServerHandlerDef.ts +105 -0
- package/src/providers/findMcpServerDef.ts +31 -0
- package/src/providers/prometheus/def.ts +21 -0
- package/src/providers/prometheus/index.ts +1 -0
- package/src/providers/relay/def.ts +31 -0
- package/src/providers/relay/index.ts +1 -0
- package/src/providers/relay/relay.test.ts +47 -0
- package/src/providers/sql/def.ts +33 -0
- package/src/providers/sql/index.ts +1 -0
- package/src/providers/tencent-cls/def.ts +38 -0
- package/src/providers/tencent-cls/index.ts +1 -0
- package/src/scripts/bundle.ts +82 -0
- package/src/server/api-routes.ts +98 -0
- package/src/server/audit.ts +310 -0
- package/src/server/chat-routes.ts +95 -0
- package/src/server/config.test.ts +162 -0
- package/src/server/config.ts +198 -0
- package/src/server/db.ts +115 -0
- package/src/server/index.ts +1 -0
- package/src/server/mcp-handler.ts +209 -0
- package/src/server/mcp-routes.ts +133 -0
- package/src/server/mcps-router.ts +133 -0
- package/src/server/schema.ts +175 -0
- package/src/server/server.ts +163 -0
- package/src/web/ChatPage.tsx +1005 -0
- package/src/web/McpInspectorPage.tsx +254 -0
- package/src/web/ServersPage.tsx +139 -0
- package/src/web/main.tsx +600 -0
- package/src/web/styles.css +15 -0
package/src/web/main.tsx
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { ChevronDown, ChevronUp, RefreshCw, Stethoscope } from 'lucide-react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { HashRouter, Routes, Route, NavLink, Navigate } from 'react-router-dom';
|
|
5
|
+
import type { AuditEvent } from '../contracts';
|
|
6
|
+
import type { ModelInfo, RequestStats, ServerInfo, ServerTypeInfo, ServiceOverview } from '../contracts/McpsContract';
|
|
7
|
+
import { ChatPage } from './ChatPage';
|
|
8
|
+
import { McpInspectorPage } from './McpInspectorPage';
|
|
9
|
+
import './styles.css';
|
|
10
|
+
|
|
11
|
+
// Simple API client using fetch
|
|
12
|
+
export const api = {
|
|
13
|
+
async overview(): Promise<ServiceOverview> {
|
|
14
|
+
const res = await fetch('/api/mcps/overview');
|
|
15
|
+
return res.json();
|
|
16
|
+
},
|
|
17
|
+
async stats(params: { from?: string; to?: string } = {}): Promise<RequestStats> {
|
|
18
|
+
const url = new URL('/api/mcps/stats', window.location.origin);
|
|
19
|
+
if (params.from) url.searchParams.set('from', params.from);
|
|
20
|
+
if (params.to) url.searchParams.set('to', params.to);
|
|
21
|
+
const res = await fetch(url);
|
|
22
|
+
return res.json();
|
|
23
|
+
},
|
|
24
|
+
async auditList(params: { limit?: number } = {}): Promise<{ events: AuditEvent[]; total: number }> {
|
|
25
|
+
const url = new URL('/api/audit', window.location.origin);
|
|
26
|
+
if (params.limit) url.searchParams.set('limit', String(params.limit));
|
|
27
|
+
const res = await fetch(url);
|
|
28
|
+
return res.json();
|
|
29
|
+
},
|
|
30
|
+
async servers(): Promise<{ servers: ServerInfo[] }> {
|
|
31
|
+
const res = await fetch('/api/mcps/servers');
|
|
32
|
+
return res.json();
|
|
33
|
+
},
|
|
34
|
+
async models(): Promise<{ models: ModelInfo[] }> {
|
|
35
|
+
const res = await fetch('/api/mcps/models');
|
|
36
|
+
return res.json();
|
|
37
|
+
},
|
|
38
|
+
async healthCheck(model: string): Promise<{ ok: boolean; error?: string; durationMs?: number }> {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch('/v1/chat/completions', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model,
|
|
46
|
+
messages: [{ role: 'user', content: 'hello' }],
|
|
47
|
+
max_tokens: 10,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const durationMs = Date.now() - start;
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const data = await res.json().catch(() => ({}));
|
|
53
|
+
return { ok: false, error: data.error?.message || `HTTP ${res.status}`, durationMs };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, durationMs };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { ok: false, error: e instanceof Error ? e.message : 'Unknown error', durationMs: Date.now() - start };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function getServerTypeBadgeClass(type: string) {
|
|
63
|
+
const classes: Record<string, string> = {
|
|
64
|
+
'tencent-cls': 'badge-info',
|
|
65
|
+
sql: 'badge-success',
|
|
66
|
+
prometheus: 'badge-secondary',
|
|
67
|
+
relay: 'badge-warning',
|
|
68
|
+
};
|
|
69
|
+
return classes[type] || 'badge-primary';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Header hints for each server type
|
|
73
|
+
const serverTypeHeaders: Record<string, { required: string[]; optional?: string[] }> = {
|
|
74
|
+
'tencent-cls': {
|
|
75
|
+
required: ['X-CLS-SECRET-ID', 'X-CLS-SECRET-KEY', 'X-CLS-REGION'],
|
|
76
|
+
optional: ['X-CLS-ENDPOINT'],
|
|
77
|
+
},
|
|
78
|
+
sql: {
|
|
79
|
+
required: ['X-DB-URL'],
|
|
80
|
+
optional: ['X-DB-READ-URL', 'X-DB-WRITE-URL'],
|
|
81
|
+
},
|
|
82
|
+
prometheus: {
|
|
83
|
+
required: ['X-SERVICE-URL'],
|
|
84
|
+
},
|
|
85
|
+
relay: {
|
|
86
|
+
required: ['X-MCP-URL'],
|
|
87
|
+
optional: ['X-MCP-TYPE', 'X-MCP-COMMAND'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function ServerTypeCard({ serverType }: { serverType: ServerTypeInfo }) {
|
|
92
|
+
const headers = serverTypeHeaders[serverType.type];
|
|
93
|
+
return (
|
|
94
|
+
<div className='card bg-base-100 shadow-sm border border-base-300'>
|
|
95
|
+
<div className='card-body p-4'>
|
|
96
|
+
<div className='flex items-center gap-2 mb-2'>
|
|
97
|
+
<span className={`badge ${getServerTypeBadgeClass(serverType.type)}`}>{serverType.type}</span>
|
|
98
|
+
</div>
|
|
99
|
+
<p className='text-sm text-base-content/70 mb-2'>{serverType.description}</p>
|
|
100
|
+
<code className='text-xs bg-base-200 px-2 py-1 rounded block mb-2'>{serverType.dynamicEndpoint}</code>
|
|
101
|
+
{headers && (
|
|
102
|
+
<div className='text-xs'>
|
|
103
|
+
<div className='text-base-content/50 mb-1'>Headers:</div>
|
|
104
|
+
<div className='flex flex-wrap gap-1'>
|
|
105
|
+
{headers.required.map((h) => (
|
|
106
|
+
<code key={h} className='bg-error/10 text-error px-1 rounded'>
|
|
107
|
+
{h}
|
|
108
|
+
</code>
|
|
109
|
+
))}
|
|
110
|
+
{headers.optional?.map((h) => (
|
|
111
|
+
<code key={h} className='bg-base-200 px-1 rounded'>
|
|
112
|
+
{h}
|
|
113
|
+
</code>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ServerCard({ server }: { server: ServerInfo }) {
|
|
124
|
+
return (
|
|
125
|
+
<div className='card bg-base-100 shadow-sm border border-base-300'>
|
|
126
|
+
<div className='card-body p-4 flex-row justify-between items-center'>
|
|
127
|
+
<div>
|
|
128
|
+
<div className='font-semibold text-base-content'>{server.name}</div>
|
|
129
|
+
<span className={`badge badge-sm ${getServerTypeBadgeClass(server.type)}`}>{server.type}</span>
|
|
130
|
+
</div>
|
|
131
|
+
{server.disabled && <span className='badge badge-error badge-sm'>disabled</span>}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ModelCard({ model }: { model: ModelInfo }) {
|
|
138
|
+
const [checking, setChecking] = useState(false);
|
|
139
|
+
const [result, setResult] = useState<{ ok: boolean; error?: string; durationMs?: number } | null>(null);
|
|
140
|
+
|
|
141
|
+
const handleCheck = async () => {
|
|
142
|
+
setChecking(true);
|
|
143
|
+
setResult(null);
|
|
144
|
+
const res = await api.healthCheck(model.name);
|
|
145
|
+
setResult(res);
|
|
146
|
+
setChecking(false);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className='card bg-base-100 shadow-sm border border-base-300'>
|
|
151
|
+
<div className='card-body p-4'>
|
|
152
|
+
<div className='flex justify-between items-start'>
|
|
153
|
+
<div className='font-semibold text-base-content mb-2'>{model.name}</div>
|
|
154
|
+
<button
|
|
155
|
+
type='button'
|
|
156
|
+
className='btn btn-xs btn-ghost'
|
|
157
|
+
onClick={handleCheck}
|
|
158
|
+
disabled={checking}
|
|
159
|
+
title='Health check'
|
|
160
|
+
>
|
|
161
|
+
{checking ? <span className='loading loading-spinner loading-xs' /> : <Stethoscope className='w-4 h-4' />}
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
<div className='flex gap-2 flex-wrap'>
|
|
165
|
+
{model.adapter && <span className='badge badge-secondary badge-sm'>{model.adapter}</span>}
|
|
166
|
+
{model.baseUrl && (
|
|
167
|
+
<span className='text-xs text-base-content/50 truncate max-w-48' title={model.baseUrl}>
|
|
168
|
+
{model.baseUrl}
|
|
169
|
+
</span>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
{result && (
|
|
173
|
+
<div className={`mt-2 text-xs ${result.ok ? 'text-success' : 'text-error'}`}>
|
|
174
|
+
{result.ok ? `✓ OK (${result.durationMs}ms)` : `✗ ${result.error}`}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Overview Page
|
|
183
|
+
function OverviewPage() {
|
|
184
|
+
const [overview, setOverview] = useState<ServiceOverview | null>(null);
|
|
185
|
+
const [stats, setStats] = useState<RequestStats | null>(null);
|
|
186
|
+
const [loading, setLoading] = useState(true);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
Promise.all([api.overview(), api.stats()])
|
|
190
|
+
.then(([o, s]) => {
|
|
191
|
+
setOverview(o);
|
|
192
|
+
setStats(s);
|
|
193
|
+
})
|
|
194
|
+
.finally(() => setLoading(false));
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
if (loading) return <div className='loading loading-spinner loading-lg mx-auto' />;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className='space-y-6'>
|
|
201
|
+
{/* Stats Cards */}
|
|
202
|
+
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
|
|
203
|
+
<div className='stat bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
204
|
+
<div className='stat-value text-2xl'>{overview?.servers.length ?? 0}</div>
|
|
205
|
+
<div className='stat-desc'>Configured Servers</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div className='stat bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
208
|
+
<div className='stat-value text-2xl'>{overview?.models.length ?? 0}</div>
|
|
209
|
+
<div className='stat-desc'>Model Configs</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div className='stat bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
212
|
+
<div className='stat-value text-2xl'>{stats?.totalRequests ?? 0}</div>
|
|
213
|
+
<div className='stat-desc'>Total Requests</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div className='stat bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
216
|
+
<div className={`stat-value text-2xl ${(stats?.totalErrors ?? 0) > 0 ? 'text-error' : ''}`}>
|
|
217
|
+
{stats?.totalErrors ?? 0}
|
|
218
|
+
</div>
|
|
219
|
+
<div className='stat-desc'>Errors</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Supported Server Types */}
|
|
224
|
+
<div>
|
|
225
|
+
<h2 className='text-lg font-semibold mb-3 text-base-content'>Supported Server Types</h2>
|
|
226
|
+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
|
227
|
+
{overview?.serverTypes.map((st) => (
|
|
228
|
+
<ServerTypeCard key={st.type} serverType={st} />
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
<div className='mt-3 text-sm text-base-content/50'>
|
|
232
|
+
<span className='font-medium'>Common Headers:</span>{' '}
|
|
233
|
+
<code className='bg-base-200 px-1 rounded'>X-MCP-Readonly</code> (TRUE = only readonly tools){' '}
|
|
234
|
+
<code className='bg-base-200 px-1 rounded'>X-MCP-Include</code>{' '}
|
|
235
|
+
<code className='bg-base-200 px-1 rounded'>X-MCP-Exclude</code> (glob patterns for tool filtering)
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Configured Servers Preview */}
|
|
240
|
+
{overview && overview.servers.length > 0 && (
|
|
241
|
+
<div>
|
|
242
|
+
<h2 className='text-lg font-semibold mb-3 text-base-content'>
|
|
243
|
+
Configured Servers ({overview.servers.length})
|
|
244
|
+
</h2>
|
|
245
|
+
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
|
246
|
+
{overview.servers.slice(0, 4).map((s) => (
|
|
247
|
+
<ServerCard key={s.name} server={s} />
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
{overview.servers.length > 4 && (
|
|
251
|
+
<div className='text-center mt-4'>
|
|
252
|
+
<NavLink to='/servers' className='btn btn-ghost btn-sm'>
|
|
253
|
+
View all {overview.servers.length} servers →
|
|
254
|
+
</NavLink>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{/* Request Stats by Method */}
|
|
261
|
+
{stats && stats.byMethod.length > 0 && (
|
|
262
|
+
<div>
|
|
263
|
+
<h2 className='text-lg font-semibold mb-3 text-base-content'>Requests by Method</h2>
|
|
264
|
+
<div className='overflow-x-auto bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
265
|
+
<table className='table table-zebra'>
|
|
266
|
+
<thead>
|
|
267
|
+
<tr>
|
|
268
|
+
<th>Method</th>
|
|
269
|
+
<th>Count</th>
|
|
270
|
+
</tr>
|
|
271
|
+
</thead>
|
|
272
|
+
<tbody>
|
|
273
|
+
{stats.byMethod.map((item) => (
|
|
274
|
+
<tr key={item.method}>
|
|
275
|
+
<td>
|
|
276
|
+
<span className='badge badge-info badge-sm'>{item.method}</span>
|
|
277
|
+
</td>
|
|
278
|
+
<td>{item.count}</td>
|
|
279
|
+
</tr>
|
|
280
|
+
))}
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Servers Page
|
|
291
|
+
function ServersPage() {
|
|
292
|
+
const [servers, setServers] = useState<ServerInfo[]>([]);
|
|
293
|
+
const [loading, setLoading] = useState(true);
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
api.servers().then((data) => {
|
|
297
|
+
setServers(data.servers);
|
|
298
|
+
setLoading(false);
|
|
299
|
+
});
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
if (loading) return <div className='loading loading-spinner loading-lg mx-auto' />;
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div>
|
|
306
|
+
<h2 className='text-lg font-semibold mb-3 text-base-content'>Configured Servers ({servers.length})</h2>
|
|
307
|
+
{servers.length > 0 ? (
|
|
308
|
+
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
|
309
|
+
{servers.map((s) => (
|
|
310
|
+
<ServerCard key={s.name} server={s} />
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
) : (
|
|
314
|
+
<div className='card bg-base-100 shadow-sm border border-base-300'>
|
|
315
|
+
<div className='card-body text-center text-base-content/50'>No servers configured</div>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Models Page
|
|
323
|
+
function ModelsPage() {
|
|
324
|
+
const [models, setModels] = useState<ModelInfo[]>([]);
|
|
325
|
+
const [loading, setLoading] = useState(true);
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
api.models().then((data) => {
|
|
329
|
+
setModels(data.models);
|
|
330
|
+
setLoading(false);
|
|
331
|
+
});
|
|
332
|
+
}, []);
|
|
333
|
+
|
|
334
|
+
if (loading) return <div className='loading loading-spinner loading-lg mx-auto' />;
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div>
|
|
338
|
+
<h2 className='text-lg font-semibold mb-3 text-base-content'>Model Configurations ({models.length})</h2>
|
|
339
|
+
{models.length > 0 ? (
|
|
340
|
+
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
|
341
|
+
{models.map((m) => (
|
|
342
|
+
<ModelCard key={m.name} model={m} />
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
) : (
|
|
346
|
+
<div className='card bg-base-100 shadow-sm border border-base-300'>
|
|
347
|
+
<div className='card-body text-center text-base-content/50'>No models configured</div>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Log Row with expand support
|
|
355
|
+
function LogRow({ event }: { event: AuditEvent }) {
|
|
356
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
357
|
+
|
|
358
|
+
const hasDetails = event.requestBody || event.responseBody || event.requestHeaders;
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<>
|
|
362
|
+
<tr className='hover'>
|
|
363
|
+
<td className='text-xs'>{new Date(event.timestamp).toLocaleTimeString()}</td>
|
|
364
|
+
<td>
|
|
365
|
+
<span className='badge badge-info badge-xs'>{event.method}</span>
|
|
366
|
+
</td>
|
|
367
|
+
<td>
|
|
368
|
+
<code className='text-xs truncate max-w-48 block' title={event.path}>
|
|
369
|
+
{event.path}
|
|
370
|
+
</code>
|
|
371
|
+
</td>
|
|
372
|
+
<td className='text-xs'>{event.serverName || event.serverType || '-'}</td>
|
|
373
|
+
<td>
|
|
374
|
+
<span className={event.status && event.status >= 400 ? 'text-error' : 'text-success'}>
|
|
375
|
+
{event.status || '-'}
|
|
376
|
+
</span>
|
|
377
|
+
</td>
|
|
378
|
+
<td className='text-xs'>{event.durationMs != null ? `${event.durationMs}ms` : '-'}</td>
|
|
379
|
+
<td>
|
|
380
|
+
{hasDetails && (
|
|
381
|
+
<button type='button' className='btn btn-ghost btn-xs' onClick={() => setIsExpanded(!isExpanded)}>
|
|
382
|
+
{isExpanded ? <ChevronUp className='w-3 h-3' /> : <ChevronDown className='w-3 h-3' />}
|
|
383
|
+
</button>
|
|
384
|
+
)}
|
|
385
|
+
</td>
|
|
386
|
+
</tr>
|
|
387
|
+
{isExpanded && hasDetails && (
|
|
388
|
+
<tr>
|
|
389
|
+
<td colSpan={7} className='bg-base-200 p-3'>
|
|
390
|
+
<div className='space-y-2 text-xs'>
|
|
391
|
+
{event.requestHeaders && (
|
|
392
|
+
<div>
|
|
393
|
+
<div className='font-semibold mb-1'>Request Headers:</div>
|
|
394
|
+
<pre className='bg-base-100 p-2 rounded overflow-auto max-h-24'>
|
|
395
|
+
{JSON.stringify(event.requestHeaders, null, 2)}
|
|
396
|
+
</pre>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
{event.requestBody != null && (
|
|
400
|
+
<div>
|
|
401
|
+
<div className='font-semibold mb-1'>Request Body:</div>
|
|
402
|
+
<pre className='bg-base-100 p-2 rounded overflow-auto max-h-48'>
|
|
403
|
+
{typeof event.requestBody === 'string'
|
|
404
|
+
? event.requestBody
|
|
405
|
+
: JSON.stringify(event.requestBody as object, null, 2)}
|
|
406
|
+
</pre>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
{event.responseBody != null && (
|
|
410
|
+
<div>
|
|
411
|
+
<div className='font-semibold mb-1'>Response Body:</div>
|
|
412
|
+
<pre className='bg-base-100 p-2 rounded overflow-auto max-h-48'>
|
|
413
|
+
{typeof event.responseBody === 'string'
|
|
414
|
+
? event.responseBody
|
|
415
|
+
: JSON.stringify(event.responseBody as object, null, 2)}
|
|
416
|
+
</pre>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
{event.error && (
|
|
420
|
+
<div>
|
|
421
|
+
<div className='font-semibold text-error mb-1'>Error:</div>
|
|
422
|
+
<pre className='bg-error/10 text-error p-2 rounded'>{event.error}</pre>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
</td>
|
|
427
|
+
</tr>
|
|
428
|
+
)}
|
|
429
|
+
</>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Logs Page (renamed from Audit)
|
|
434
|
+
function LogsPage() {
|
|
435
|
+
const [events, setEvents] = useState<AuditEvent[]>([]);
|
|
436
|
+
const [loading, setLoading] = useState(true);
|
|
437
|
+
const [tab, setTab] = useState<'all' | 'mcp' | 'chat'>('all');
|
|
438
|
+
|
|
439
|
+
const fetchLogs = () => {
|
|
440
|
+
setLoading(true);
|
|
441
|
+
api.auditList({ limit: 200 }).then((data) => {
|
|
442
|
+
setEvents(data.events);
|
|
443
|
+
setLoading(false);
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
fetchLogs();
|
|
449
|
+
const interval = setInterval(fetchLogs, 5000);
|
|
450
|
+
return () => clearInterval(interval);
|
|
451
|
+
}, []);
|
|
452
|
+
|
|
453
|
+
const mcpLogs = events.filter((e) => e.path.startsWith('/mcp/'));
|
|
454
|
+
const chatLogs = events.filter((e) => e.path.startsWith('/v1/'));
|
|
455
|
+
const allLogs = events;
|
|
456
|
+
const currentLogs = tab === 'all' ? allLogs : tab === 'mcp' ? mcpLogs : chatLogs;
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div>
|
|
460
|
+
<div className='flex items-center justify-between mb-3'>
|
|
461
|
+
<h2 className='text-lg font-semibold text-base-content'>Logs</h2>
|
|
462
|
+
<button type='button' className='btn btn-ghost btn-sm gap-1' onClick={fetchLogs} disabled={loading}>
|
|
463
|
+
{loading ? <span className='loading loading-spinner loading-xs' /> : <RefreshCw className='w-4 h-4' />}
|
|
464
|
+
Refresh
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<div className='tabs tabs-boxed mb-4 bg-base-100 w-fit'>
|
|
469
|
+
<button type='button' className={`tab ${tab === 'all' ? 'tab-active' : ''}`} onClick={() => setTab('all')}>
|
|
470
|
+
All ({allLogs.length})
|
|
471
|
+
</button>
|
|
472
|
+
<button type='button' className={`tab ${tab === 'mcp' ? 'tab-active' : ''}`} onClick={() => setTab('mcp')}>
|
|
473
|
+
MCP ({mcpLogs.length})
|
|
474
|
+
</button>
|
|
475
|
+
<button type='button' className={`tab ${tab === 'chat' ? 'tab-active' : ''}`} onClick={() => setTab('chat')}>
|
|
476
|
+
Chat ({chatLogs.length})
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div className='overflow-x-auto bg-base-100 rounded-box shadow-sm border border-base-300'>
|
|
481
|
+
<table className='table table-zebra table-sm'>
|
|
482
|
+
<thead>
|
|
483
|
+
<tr>
|
|
484
|
+
<th>Time</th>
|
|
485
|
+
<th>Method</th>
|
|
486
|
+
<th>Path</th>
|
|
487
|
+
<th>Server/Type</th>
|
|
488
|
+
<th>Status</th>
|
|
489
|
+
<th>Duration</th>
|
|
490
|
+
<th></th>
|
|
491
|
+
</tr>
|
|
492
|
+
</thead>
|
|
493
|
+
<tbody>
|
|
494
|
+
{currentLogs.map((event) => (
|
|
495
|
+
<LogRow key={event.id} event={event} />
|
|
496
|
+
))}
|
|
497
|
+
{currentLogs.length === 0 && (
|
|
498
|
+
<tr>
|
|
499
|
+
<td colSpan={7} className='text-center text-base-content/50'>
|
|
500
|
+
No logs yet
|
|
501
|
+
</td>
|
|
502
|
+
</tr>
|
|
503
|
+
)}
|
|
504
|
+
</tbody>
|
|
505
|
+
</table>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Chat Page Wrapper
|
|
512
|
+
function ChatPageWrapper() {
|
|
513
|
+
return (
|
|
514
|
+
<div className='card bg-base-100 shadow-sm border border-base-300 h-[calc(100vh-200px)]'>
|
|
515
|
+
<ChatPage />
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Inspector Page Wrapper
|
|
521
|
+
function InspectorPageWrapper() {
|
|
522
|
+
return (
|
|
523
|
+
<div className='card bg-base-100 shadow-sm border border-base-300 h-[calc(100vh-200px)]'>
|
|
524
|
+
<McpInspectorPage />
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Layout
|
|
530
|
+
function Layout({ children }: { children: React.ReactNode }) {
|
|
531
|
+
const [overview, setOverview] = useState<ServiceOverview | null>(null);
|
|
532
|
+
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
api.overview().then(setOverview);
|
|
535
|
+
}, []);
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div className='min-h-screen bg-base-200'>
|
|
539
|
+
<div className='container mx-auto max-w-7xl p-4 md:p-6'>
|
|
540
|
+
{/* Header */}
|
|
541
|
+
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6'>
|
|
542
|
+
<div>
|
|
543
|
+
<h1 className='text-2xl font-bold text-base-content'>MCPS Dashboard</h1>
|
|
544
|
+
<p className='text-sm text-base-content/60'>
|
|
545
|
+
{overview?.name} v{overview?.version}
|
|
546
|
+
</p>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
{/* Navigation */}
|
|
551
|
+
<nav className='tabs tabs-boxed mb-6 bg-base-100'>
|
|
552
|
+
<NavLink to='/' end className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
553
|
+
Overview
|
|
554
|
+
</NavLink>
|
|
555
|
+
<NavLink to='/servers' className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
556
|
+
Servers
|
|
557
|
+
</NavLink>
|
|
558
|
+
<NavLink to='/models' className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
559
|
+
Models
|
|
560
|
+
</NavLink>
|
|
561
|
+
<NavLink to='/logs' className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
562
|
+
Logs
|
|
563
|
+
</NavLink>
|
|
564
|
+
<NavLink to='/chat' className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
565
|
+
Chat
|
|
566
|
+
</NavLink>
|
|
567
|
+
<NavLink to='/inspector' className={({ isActive }) => `tab ${isActive ? 'tab-active' : ''}`}>
|
|
568
|
+
Inspector
|
|
569
|
+
</NavLink>
|
|
570
|
+
</nav>
|
|
571
|
+
|
|
572
|
+
{/* Content */}
|
|
573
|
+
{children}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function App() {
|
|
580
|
+
return (
|
|
581
|
+
<HashRouter>
|
|
582
|
+
<Layout>
|
|
583
|
+
<Routes>
|
|
584
|
+
<Route path='/' element={<OverviewPage />} />
|
|
585
|
+
<Route path='/servers' element={<ServersPage />} />
|
|
586
|
+
<Route path='/models' element={<ModelsPage />} />
|
|
587
|
+
<Route path='/logs' element={<LogsPage />} />
|
|
588
|
+
<Route path='/chat' element={<ChatPageWrapper />} />
|
|
589
|
+
<Route path='/inspector' element={<InspectorPageWrapper />} />
|
|
590
|
+
<Route path='*' element={<Navigate to='/' replace />} />
|
|
591
|
+
</Routes>
|
|
592
|
+
</Layout>
|
|
593
|
+
</HashRouter>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const rootEl = document.getElementById('root');
|
|
598
|
+
if (!rootEl) throw new Error('Root element not found');
|
|
599
|
+
const root = createRoot(rootEl);
|
|
600
|
+
root.render(<App />);
|