Skip to main content

Devlog Feb 5: MCP Implementation (Part 2)

· 6 min read
Eduardez
MoLOS Lead Developer

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>

View commits on GitHub

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.