Devlog Feb 5: MCP Implementation (Part 2)
Building on Part 1's server infrastructure, Part 2 focuses on the MCP dashboard UI, providing an intuitive interface for managing resources, prompts, and API keys.
Material Design 3 Architecture
We redesigned the MCP page following Material Design 3 principles:
Component Structure
The dashboard is organized into modular components:
// src/lib/components/ai/mcp/McpDashboard.svelte
<script lang="ts">
import McpResourcesPanel from './McpResourcesPanel.svelte';
import McpPromptsPanel from './McpPromptsPanel.svelte';
import McpApiKeysPanel from './McpApiKeysPanel.svelte';
import McpActivityLog from './McpActivityLog.svelte';
</script>
<div class="mcp-dashboard">
<McpResourcesPanel />
<McpPromptsPanel />
<McpApiKeysPanel />
<McpActivityLog />
</div>
Color System
We implemented semantic color tokens following MD3 guidelines:
/* src/lib/components/ai/mcp/dashboard.module.css */
:root {
--md-sys-color-primary: #6750A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #EADDFF;
--md-sys-color-on-primary-container: #21005D;
--md-sys-color-secondary: #625B71;
--md-sys-color-surface: #FEF7FF;
--md-sys-color-surface-variant: #E7E0EC;
}
.primary-button {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 20px;
padding: 10px 24px;
}
Typography Scale
Using the MD3 typography scale for consistent hierarchy:
.display-large {
font-size: 57px;
line-height: 64px;
font-weight: 400;
}
.headline-medium {
font-size: 28px;
line-height: 36px;
font-weight: 400;
}
.title-medium {
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
.body-medium {
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
Resources Management
CRUD Operations
The resources panel provides full CRUD functionality:
// src/lib/components/ai/mcp/McpResourcesPanel.svelte
<script lang="ts">
import { writable } from 'svelte/store';
import CreateResourceDialog from './dialogs/CreateResourceDialog.svelte';
import EditResourceDialog from './dialogs/EditResourceDialog.svelte';
import DeleteResourceDialog from './dialogs/DeleteResourceDialog.svelte';
const resources = writable<McpResource[]>([]);
const showCreateDialog = writable(false);
const selectedResource = writable<McpResource | null>(null);
async function loadResources() {
const response = await fetch('/api/ai/mcp/resources');
resources.set(await response.json());
}
$: loadResources();
</script>
Resource Types
We support multiple resource types with type-specific configuration:
interface McpResource {
id: string;
name: string;
uri: string;
type: 'database' | 'api' | 'file' | 'url';
mimeType: string;
config: ResourceConfig;
scopes: string[];
createdAt: Date;
}
interface ResourceConfig {
[key: string]: unknown;
// Database: { table, columns, filters }
// API: { endpoint, method, headers, auth }
// File: { path, encoding }
// URL: { cacheTimeout, followRedirects }
}
Create Dialog
// src/lib/components/ai/mcp/dialogs/CreateResourceDialog.svelte
<script lang="ts">
export let open: boolean;
let resourceType: McpResource['type'] = 'database';
let name = '';
let uri = '';
async function createResource() {
const response = await fetch('/api/ai/mcp/resources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: resourceType, name, uri })
});
if (response.ok) {
open = false;
// Refresh resources list
}
}
</script>
<Dialog bind:open title="Create Resource">
<Select label="Resource Type" bind:value={resourceType}>
<option value="database">Database</option>
<option value="api">API</option>
<option value="file">File</option>
<option value="url">URL</option>
</Select>
<TextField label="Name" bind:value={name} />
<TextField label="URI" bind:value={uri} />
<Button on:click={createResource}>Create</Button>
</Dialog>
Prompts Management
Prompt Templates
Prompts are stored as templates with parameter substitution:
interface McpPrompt {
id: string;
name: string;
description: string;
template: string;
parameters: PromptParameter[];
arguments?: Record<string, unknown>;
createdAt: Date;
}
interface PromptParameter {
name: string;
description: string;
type: 'string' | 'number' | 'boolean' | 'object';
required: boolean;
default?: unknown;
}
Prompt Execution
// src/routes/api/ai/mcp/prompts/[name]/+server.ts
import { compileTemplate } from '$lib/server/mcp/template-compiler';
export const GET: RequestHandler = async ({ params, url }) => {
const prompt = await getPrompt(params.name);
const args = Object.fromEntries(url.searchParams);
const compiled = compileTemplate(prompt.template, args);
return json({
messages: [
{
role: 'user',
content: {
type: 'text',
text: compiled
}
}
]
});
};
Template Syntax
We support a simple template syntax:
{{parameter}}
{{#condition}}...{{/condition}}
{{#each items}}{{this}}{{/each}}
API Key Management
Key Generation
API keys are generated using cryptographically secure random values:
// src/lib/server/mcp/api-key-service.ts
import crypto from 'crypto';
export async function generateApiKey(): Promise<string> {
const prefix = 'mols_';
const secret = crypto.randomBytes(32).toString('hex');
return `${prefix}${secret}`;
}
export async function createApiKey(
options: CreateApiKeyOptions
): Promise<ApiKey> {
const key = await generateApiKey();
const hash = await hashApiKey(key);
return await db.insert(apiKeys).values({
id: crypto.randomUUID(),
keyHash: hash,
name: options.name,
modules: options.modules,
scopes: options.scopes,
expiresAt: options.expiresAt,
createdAt: new Date()
});
}
Multi-Select Module Access
API keys can be scoped to specific modules using a multi-select interface:
// src/lib/components/ai/mcp/dialogs/CreateApiKeyDialog.svelte
<script lang="ts">
const allModules = writable<Module[]>([]);
const selectedModules = writable<string[]>([]);
async function loadModules() {
const response = await fetch('/api/modules');
allModules.set(await response.json());
}
loadModules();
</script>
<MultiSelect
label="Modules"
options={$allModules}
bind:selected={selectedModules}
displayProperty="name"
valueProperty="id"
/>
Edit & Revoke
API keys can be edited or revoked through dedicated dialogs:
// src/lib/components/ai/mcp/dialogs/EditApiKeyDialog.svelte
<script lang="ts">
export let apiKey: ApiKey;
async function revokeApiKey() {
const response = await fetch(`/api/ai/mcp/api-keys/${apiKey.id}`, {
method: 'DELETE'
});
if (response.ok) {
showRevokeConfirmation.set(false);
// Refresh API keys list
}
}
</script>
<Dialog title="Revoke API Key" bind:open={showRevokeConfirmation}>
<p>Are you sure you want to revoke {apiKey.name}?</p>
<div class="actions">
<Button variant="outlined" on:click={() => showRevokeConfirmation.set(false)}>
Cancel
</Button>
<Button variant="filled" on:click={revokeApiKey}>
Revoke
</Button>
</div>
</Dialog>
Activity Log
Log Entries
Activity logs track all MCP operations:
interface McpActivityLog {
id: string;
timestamp: Date;
operation: 'read_resource' | 'list_resources' | 'get_prompt' | 'list_prompts';
apiKeyId: string;
resourceUri?: string;
promptName?: string;
status: 'success' | 'error';
durationMs?: number;
errorMessage?: string;
}
Paginated Table
// src/lib/components/ai/mcp/McpActivityLog.svelte
<script lang="ts">
const logs = writable<McpActivityLog[]>([]);
const page = writable(0);
const pageSize = 20;
async function loadLogs() {
const response = await fetch(
`/api/ai/mcp/logs?page=${$page}&size=${pageSize}`
);
logs.set(await response.json());
}
$: page.subscribe(loadLogs);
</script>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Operation</th>
<th>Resource/Prompt</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{#each $logs as log}
<tr>
<td>{formatDate(log.timestamp)}</td>
<td>{log.operation}</td>
<td>{log.resourceUri || log.promptName}</td>
<td>{log.status}</td>
<td>{log.durationMs}ms</td>
</tr>
{/each}
</tbody>
</table>
<Pagination bind:page={$page} pageSize={pageSize} total={totalLogs} />
Help System
We added an integrated help dialog system for user guidance:
// src/lib/components/ai/mcp/McpHelpDialog.svelte
<script lang="ts">
export let open: boolean;
export let topic: 'resources' | 'prompts' | 'api-keys' | 'general';
const helpContent: Record<string, string> = {
resources: `
# MCP Resources
Resources represent external data that AI models can access.
**Creating a Resource:**
1. Click "Create Resource"
2. Choose the resource type
3. Configure the connection parameters
4. Save to generate the URI
**Supported Types:**
- Database: Query database tables
- API: Access REST endpoints
- File: Read file system data
- URL: Fetch web resources
`,
// ... more help content
};
</script>
<Dialog bind:open title="Help: {topic}">
{@html renderMarkdown(helpContent[topic])}
</Dialog>
What's Next
Upcoming work includes:
- Enhanced resource types with streaming support
- Prompt templates with dynamic content injection
- MCP client libraries for Python and JavaScript
- Performance optimizations and caching strategies
Read the MCP Announcement for the feature overview.
