feat(mcp): add server logging system with modal viewer

- Add comprehensive logging for all MCP tool calls with timestamps,
  duration, arguments, results, and session tracking
- Implement McpLogModal component for viewing logs in settings UI
- Add log truncation for large responses (max 500 chars preview)
- Add getServer() method to McpServerManager for log access
- Fix metadata type safety by checking string type before trim()
- Normalize project names to handle arrays and non-string values
This commit is contained in:
Quorafind 2025-11-12 20:53:08 +08:00
parent 1464780e2d
commit 586e8613db
15 changed files with 1098 additions and 353 deletions

View file

@ -8,6 +8,7 @@ import TaskProgressBarPlugin from "@/index";
import { McpServerManager } from "@/mcp/McpServerManager";
import { AuthMiddleware } from "@/mcp/auth/AuthMiddleware";
import { ConfirmModal } from "@/components/ui/modals/ConfirmModal";
import { McpLogModal } from "@/components/ui/modals/McpLogModal";
import "@/styles/mcp-integration.css";
import { TaskProgressBarSettingTab } from "@/setting";
@ -521,6 +522,26 @@ export function renderMcpIntegrationSettingsTab(
});
});
// View Logs Button
new Setting(actionsContainer)
.setName(t("View Server Logs"))
.setDesc(t("View all MCP server tool calls and their results"))
.addButton((button) => {
button
.setButtonText(t("View Logs"))
.setIcon("file-text")
.onClick(() => {
const server = plugin.mcpServerManager?.getServer();
if (server) {
const logs = server.getLogs();
const modal = new McpLogModal(plugin, logs);
modal.open();
} else {
new Notice(t("MCP server is not running"));
}
});
});
// Client Configuration
containerEl.createEl("h3", { text: t("Client Configuration") });

View file

@ -1295,7 +1295,7 @@ export class TableRenderer extends Component {
if (
isProjectColumn &&
row?.task?.metadata?.tgProject &&
(!row.task.metadata.project || !row.task.metadata.project.trim())
(!row.task.metadata.project || typeof row.task.metadata.project !== 'string' || !row.task.metadata.project.trim())
) {
const tgProject = row.task.metadata.tgProject;
const indicator = cellEl.createDiv({
@ -1574,12 +1574,12 @@ export class TableRenderer extends Component {
});
break;
case "project":
if (task.metadata.project && task.metadata.project.trim()) {
if (task.metadata.project && typeof task.metadata.project === 'string' && task.metadata.project.trim()) {
values.add(task.metadata.project);
}
break;
case "context":
if (task.metadata.context && task.metadata.context.trim()) {
if (task.metadata.context && typeof task.metadata.context === 'string' && task.metadata.context.trim()) {
values.add(task.metadata.context);
}
break;

View file

@ -409,7 +409,7 @@ export class TaskMetadataEditor extends Component {
if (
isReadonly &&
this.task.metadata.tgProject &&
(!this.task.metadata.project || !this.task.metadata.project.trim())
(!this.task.metadata.project || typeof this.task.metadata.project !== 'string' || !this.task.metadata.project.trim())
) {
fieldContainer.addClass("project-readonly");
const indicator = fieldContainer.createDiv({

View file

@ -349,7 +349,7 @@ export class InlineEditor extends Component {
// Add visual indicator for tgProject - only show if no user-set project exists
if (
this.task.metadata.tgProject &&
(!this.task.metadata.project || !this.task.metadata.project.trim())
(!this.task.metadata.project || typeof this.task.metadata.project !== 'string' || !this.task.metadata.project.trim())
) {
const tgProject = this.task.metadata.tgProject;
const indicator = container.createDiv({

View file

@ -363,7 +363,7 @@ export class TaskDetailsComponent extends Component {
// Add visual indicator for tgProject - only show if no user-set project exists
if (
task.metadata.tgProject &&
(!task.metadata.project || !task.metadata.project.trim())
(!task.metadata.project || typeof task.metadata.project !== 'string' || !task.metadata.project.trim())
) {
const tgProject = task.metadata.tgProject;
const indicator = projectField.createDiv({

View file

@ -0,0 +1,237 @@
import { Modal, ButtonComponent, ExtraButtonComponent } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { McpLogEntry } from "@/mcp/McpServer";
import "@/styles/modal.css";
import { formatDate } from "@/utils/date/date-formatter";
/**
* Modal for displaying MCP server logs
*/
export class McpLogModal extends Modal {
private plugin: TaskProgressBarPlugin;
private logs: McpLogEntry[];
private logsContainerEl: HTMLElement;
private searchInput: HTMLInputElement;
private filteredLogs: McpLogEntry[];
constructor(plugin: TaskProgressBarPlugin, logs: McpLogEntry[]) {
super(plugin.app);
this.plugin = plugin;
this.logs = logs;
this.filteredLogs = logs;
}
onOpen() {
this.titleEl.setText("MCP Server Logs");
this.modalEl.addClass("mcp-log-modal");
// Header section with search and clear
const headerEl = this.contentEl.createDiv({ cls: "mcp-log-header" });
// Search input
const searchContainer = headerEl.createDiv({
cls: "mcp-log-search-container",
});
this.searchInput = searchContainer.createEl("input", {
type: "text",
placeholder: "Search logs...",
cls: "mcp-log-search-input",
});
this.searchInput.addEventListener("input", () => this.filterLogs());
// Clear logs button
new ButtonComponent(headerEl)
.setButtonText("Clear Logs")
.setIcon("trash")
.setWarning()
.onClick(() => {
if (this.plugin.mcpServerManager?.getServer()) {
this.plugin.mcpServerManager.getServer()?.clearLogs();
this.logs = [];
this.filteredLogs = [];
this.renderLogs();
}
});
// Stats section
const statsEl = this.contentEl.createDiv({ cls: "mcp-log-stats" });
this.renderStats(statsEl);
// Logs container with scrolling
this.logsContainerEl = this.contentEl.createDiv({
cls: "mcp-log-container",
});
this.renderLogs();
// Footer buttons
const footerEl = this.contentEl.createDiv({
cls: "modal-button-container",
});
new ButtonComponent(footerEl).setButtonText("Refresh").onClick(() => {
if (this.plugin.mcpServerManager?.getServer()) {
this.logs =
this.plugin.mcpServerManager.getServer()?.getLogs() || [];
this.filterLogs();
}
});
new ButtonComponent(footerEl).setButtonText("Close").onClick(() => {
this.close();
});
}
private filterLogs() {
const query = this.searchInput.value.toLowerCase();
if (!query) {
this.filteredLogs = this.logs;
} else {
this.filteredLogs = this.logs.filter((log) => {
return (
log.toolName?.toLowerCase().includes(query) ||
log.method.toLowerCase().includes(query) ||
log.error?.toLowerCase().includes(query) ||
JSON.stringify(log.arguments).toLowerCase().includes(query)
);
});
}
this.renderLogs();
}
private renderStats(container: HTMLElement) {
container.empty();
const totalLogs = this.logs.length;
const errorCount = this.logs.filter((log) => log.error).length;
const successCount = totalLogs - errorCount;
const statsText = container.createDiv({ cls: "mcp-log-stats-text" });
statsText.setText(
`Total: ${totalLogs} | Success: ${successCount} | Errors: ${errorCount}`
);
}
private renderLogs() {
this.logsContainerEl.empty();
if (this.filteredLogs.length === 0) {
const emptyEl = this.logsContainerEl.createDiv({
cls: "mcp-log-empty",
});
emptyEl.createEl("p", {
text:
this.logs.length === 0
? "No logs yet. Tool calls will appear here."
: "No logs match your search.",
});
return;
}
this.filteredLogs.forEach((log, index) => {
const logEntry = this.logsContainerEl.createDiv({
cls: `mcp-log-entry ${
log.error ? "mcp-log-error" : "mcp-log-success"
}`,
});
// Header row
const headerRow = logEntry.createDiv({
cls: "mcp-log-entry-header",
});
// Timestamp
const timestamp = headerRow.createDiv({
cls: "mcp-log-timestamp",
});
timestamp.setText(formatDate(log.timestamp));
// Tool name or method
const toolName = headerRow.createDiv({ cls: "mcp-log-tool-name" });
toolName.setText(log.toolName || log.method);
// Duration
const duration = headerRow.createDiv({ cls: "mcp-log-duration" });
duration.setText(`${log.duration}ms`);
// Session ID (if available)
if (log.sessionId) {
const sessionId = headerRow.createDiv({
cls: "mcp-log-session-id",
});
sessionId.setText(log.sessionId.substring(0, 12) + "...");
sessionId.setAttribute("title", log.sessionId);
}
let isExpanded: boolean = false;
// Toggle button for details
new ExtraButtonComponent(headerRow)
.setIcon("chevron-down")
.onClick(() => {
isExpanded = !isExpanded;
detailsEl.toggle(isExpanded);
});
// Details section (collapsed by default)
const detailsEl = logEntry.createDiv({
cls: "mcp-log-details",
});
detailsEl.style.display = "none";
// Arguments
if (log.arguments && Object.keys(log.arguments).length > 0) {
const argsSection = detailsEl.createDiv({
cls: "mcp-log-section",
});
argsSection.createEl("div", {
cls: "mcp-log-section-title",
text: "Arguments:",
});
const argsContent = argsSection.createEl("pre", {
cls: "mcp-log-section-content",
});
argsContent.setText(JSON.stringify(log.arguments, null, 2));
}
// Result or Error
if (log.error) {
const errorSection = detailsEl.createDiv({
cls: "mcp-log-section",
});
errorSection.createEl("div", {
cls: "mcp-log-section-title mcp-log-error-title",
text: "Error:",
});
const errorContent = errorSection.createEl("pre", {
cls: "mcp-log-section-content mcp-log-error-content",
});
errorContent.setText(log.error);
} else if (log.result) {
const resultSection = detailsEl.createDiv({
cls: "mcp-log-section",
});
const resultTitle = resultSection.createEl("div", {
cls: "mcp-log-section-title",
text: "Result:",
});
if (log.truncated) {
resultTitle.createEl("span", {
cls: "mcp-log-truncated-badge",
text: " (truncated)",
});
}
const resultContent = resultSection.createEl("pre", {
cls: "mcp-log-section-content",
});
resultContent.setText(JSON.stringify(log.result, null, 2));
}
// Toggle functionality
});
}
onClose() {
this.contentEl.empty();
}
}

View file

@ -1,2 +1,3 @@
export * from './ConfirmModal';
export * from './IframeModal';
export * from './IframeModal';
export * from './McpLogModal';

View file

@ -450,12 +450,21 @@ export class Augmentor {
? String(ctx.fileMeta?.[metadataKeyFromConfig]).trim()
: undefined;
const effectiveProjectName =
let effectiveProjectName =
ctx.projectName ||
projectFromMeta ||
projectFromMetadataKey ||
projectFromFrontmatter;
// Normalize effectiveProjectName to string: handle arrays (take first element), non-strings, etc.
if (effectiveProjectName) {
if (Array.isArray(effectiveProjectName)) {
effectiveProjectName = effectiveProjectName[0]?.toString() || undefined;
} else if (typeof effectiveProjectName !== 'string') {
effectiveProjectName = String(effectiveProjectName);
}
}
// Set project name if not already set
if (!metadata.project && effectiveProjectName) {
metadata.project = effectiveProjectName;

View file

@ -16,6 +16,21 @@ const http = require("http");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const url = require("url");
/**
* Log entry for MCP tool calls
*/
export interface McpLogEntry {
timestamp: Date;
sessionId?: string;
method: string;
toolName?: string;
arguments: any;
result: any;
error?: string;
duration: number;
truncated?: boolean;
}
export class McpServer {
private httpServer: any;
private authMiddleware: AuthMiddleware;
@ -27,6 +42,8 @@ export class McpServer {
private actualPort?: number;
private sessions: Map<string, { created: Date; lastAccess: Date }> =
new Map();
private logs: McpLogEntry[] = [];
private readonly MAX_LOGS = 1000;
constructor(
private plugin: TaskProgressBarPlugin,
@ -62,6 +79,57 @@ export class McpServer {
}
}
/**
* Truncate large data for logging
*/
private truncateForLog(data: any, maxLength: number = 500): any {
const str = JSON.stringify(data);
if (str.length <= maxLength) {
return data;
}
// Check if it's an array of tasks
if (Array.isArray(data) && data.length > 0) {
return {
_truncated: true,
_type: "array",
_count: data.length,
_sample: data.slice(0, 2), // Show first 2 items
};
}
// For other objects, just truncate the string
return {
_truncated: true,
_preview: str.substring(0, maxLength) + "...",
};
}
/**
* Add a log entry
*/
private addLog(entry: McpLogEntry): void {
this.logs.unshift(entry); // Add to beginning
// Keep only MAX_LOGS entries
if (this.logs.length > this.MAX_LOGS) {
this.logs = this.logs.slice(0, this.MAX_LOGS);
}
}
/**
* Get all logs (most recent first)
*/
public getLogs(): McpLogEntry[] {
return [...this.logs];
}
/**
* Clear all logs
*/
public clearLogs(): void {
this.logs = [];
}
/**
* Get prompt definitions
*/
@ -853,11 +921,17 @@ export class McpServer {
/**
* Execute a tool
*/
private async executeTool(toolName: string, args: any): Promise<any> {
private async executeTool(
toolName: string,
args: any,
sessionId?: string
): Promise<any> {
const startTime = Date.now();
const taskBridge = await this.requireTaskBridge();
try {
let result: any;
let result: any;
let error: string | undefined;
try {
switch (toolName) {
case "query_tasks":
result = await taskBridge.queryTasks(args);
@ -950,6 +1024,19 @@ export class McpServer {
throw new Error(`Tool not found: ${toolName}`);
}
// Log successful execution
const duration = Date.now() - startTime;
this.addLog({
timestamp: new Date(),
sessionId,
method: "tools/call",
toolName,
arguments: args,
result: this.truncateForLog(result),
duration,
truncated: JSON.stringify(result).length > 500,
});
return {
content: [
{
@ -958,17 +1045,32 @@ export class McpServer {
},
],
};
} catch (error: any) {
} catch (err: any) {
error = err.message;
const duration = Date.now() - startTime;
// Log failed execution
this.addLog({
timestamp: new Date(),
sessionId,
method: "tools/call",
toolName,
arguments: args,
result: null,
error,
duration,
});
// Re-throw tool not found errors so they can be handled properly
if (error.message.includes("Tool not found")) {
throw error;
if (err.message.includes("Tool not found")) {
throw err;
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: false, error: error.message },
{ success: false, error: err.message },
null,
2
),
@ -1095,7 +1197,8 @@ export class McpServer {
try {
const result = await this.executeTool(
toolName,
toolArgs
toolArgs,
sessionId
);
return {
jsonrpc: "2.0",
@ -1305,6 +1408,11 @@ export class McpServer {
});
req.on("end", async () => {
// Debug: log received body for UTF-8 validation
if (body.includes("content")) {
console.log("[MCP Debug] Received body:", body.substring(0, 300));
}
let request;
try {
request = JSON.parse(body);

View file

@ -234,7 +234,7 @@ export class McpServerManager {
regenerateAuthToken(): string {
const newToken = AuthMiddleware.generateToken();
this.config.authToken = newToken;
if (this.server) {
this.server.updateConfig({ authToken: newToken });
}
@ -243,6 +243,10 @@ export class McpServerManager {
return newToken;
}
getServer(): McpServer | undefined {
return this.server;
}
async cleanup(): Promise<void> {
await this.stop();
}

View file

@ -2,537 +2,571 @@
/* Status Container */
.mcp-status-container {
padding: 16px;
background: var(--background-secondary);
border-radius: 8px;
margin-bottom: 24px;
border: 1px solid var(--background-modifier-border);
padding: 16px;
background: var(--background-secondary);
border-radius: 8px;
margin-bottom: 24px;
border: 1px solid var(--background-modifier-border);
}
.mcp-status {
display: flex;
flex-direction: column;
gap: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.mcp-status-indicator {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 1.05em;
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 1.05em;
}
.mcp-status-indicator.running .status-dot {
color: var(--background-modifier-success);
font-size: 1.2em;
animation: pulse 2s infinite;
color: var(--background-modifier-success);
font-size: 1.2em;
animation: pulse 2s infinite;
}
.mcp-status-indicator.stopped .status-dot {
color: var(--text-muted);
font-size: 1.2em;
color: var(--text-muted);
font-size: 1.2em;
}
.mcp-status-details {
display: flex;
gap: 24px;
font-size: 0.9em;
color: var(--text-muted);
padding-left: 30px;
display: flex;
gap: 24px;
font-size: 0.9em;
color: var(--text-muted);
padding-left: 30px;
}
/* Client Configuration */
.mcp-config-container {
margin-bottom: 30px;
margin-bottom: 30px;
}
.mcp-client-section {
margin-bottom: 16px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
overflow: hidden;
}
.mcp-client-header {
cursor: pointer;
padding: 12px 16px;
background: var(--background-secondary);
border-radius: 0;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 10px;
user-select: none;
transition: background-color 0.2s;
cursor: pointer;
padding: 12px 16px;
background: var(--background-secondary);
border-radius: 0;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 10px;
user-select: none;
transition: background-color 0.2s;
}
.mcp-client-header:hover {
background: var(--background-modifier-hover);
background: var(--background-modifier-hover);
}
/* Arrow icon for collapsible sections */
.mcp-arrow {
transition: transform 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
transition: transform 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
}
.mcp-arrow svg {
width: 14px;
height: 14px;
stroke-width: 2;
width: 14px;
height: 14px;
stroke-width: 2;
}
.mcp-arrow.expanded {
transform: rotate(90deg);
transform: rotate(90deg);
}
.mcp-client-name {
font-weight: 500;
color: var(--text-normal);
font-weight: 500;
color: var(--text-normal);
}
.mcp-client-content {
padding: 16px !important;
background: var(--background-primary);
display: none;
padding: 16px !important;
background: var(--background-primary);
display: none;
}
.mcp-client-content.expanded {
display: block;
display: block;
}
.mcp-config-block {
position: relative;
margin-bottom: 20px;
position: relative;
margin-bottom: 20px;
}
.mcp-config-code {
position: relative;
padding: 16px 60px 16px 16px !important;
background: var(--code-background);
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
margin-bottom: 0;
font-size: 0.85em;
line-height: 1.5;
overflow: auto;
max-height: 300px;
font-family: var(--font-monospace);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-bg);
position: relative;
padding: 16px 60px 16px 16px !important;
background: var(--code-background);
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
margin-bottom: 0;
font-size: 0.85em;
line-height: 1.5;
overflow: auto;
max-height: 300px;
font-family: var(--font-monospace);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-bg);
}
.mcp-config-code code {
color: var(--code-normal);
font-family: var(--font-monospace);
white-space: pre;
display: block;
color: var(--code-normal);
font-family: var(--font-monospace);
white-space: pre;
display: block;
}
.mcp-config-code::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 8px;
height: 8px;
}
.mcp-config-code::-webkit-scrollbar-track {
background: var(--scrollbar-bg);
background: var(--scrollbar-bg);
}
.mcp-config-code::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-bg);
border-radius: 4px;
background: var(--scrollbar-thumb-bg);
border-radius: 4px;
}
.mcp-config-code::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-active-thumb-bg);
background: var(--scrollbar-active-thumb-bg);
}
/* Copy Buttons */
.mcp-copy-btn {
position: absolute !important;
top: 12px;
right: 12px;
padding: 6px 12px;
font-size: 0.8em;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
z-index: 10;
position: absolute !important;
top: 12px;
right: 12px;
padding: 6px 12px;
font-size: 0.8em;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
z-index: 10;
}
.mcp-copy-btn:hover {
background: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
background: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.mcp-copy-btn.copied {
animation: copySuccess 0.3s ease;
background: var(--background-modifier-success) !important;
animation: copySuccess 0.3s ease;
background: var(--background-modifier-success) !important;
}
/* Authentication Methods List */
.mcp-auth-methods {
margin: 15px 0;
padding: 15px;
background: var(--background-secondary);
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
margin: 15px 0;
padding: 15px;
background: var(--background-secondary);
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
}
.mcp-auth-list {
margin: 10px 0 0 20px;
line-height: 1.8;
margin: 10px 0 0 20px;
line-height: 1.8;
}
.mcp-auth-list li {
margin: 8px 0;
color: var(--text-normal);
margin: 8px 0;
color: var(--text-normal);
}
.mcp-auth-list code {
background: var(--code-background);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
color: var(--code-normal);
background: var(--code-background);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
color: var(--code-normal);
}
/* Token Input */
.mcp-token-input {
font-family: monospace;
width: 300px;
margin-right: 10px;
font-family: monospace;
width: 300px;
margin-right: 10px;
}
/* API Documentation */
.mcp-docs-container {
margin-top: 20px;
margin-top: 20px;
}
.mcp-docs-section {
margin-bottom: 30px;
margin-bottom: 30px;
}
.mcp-docs-subtitle {
font-size: 1em;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-normal);
font-size: 1em;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-normal);
}
/* Endpoint Box */
.mcp-endpoint-box {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
margin-bottom: 20px;
}
.mcp-endpoint-content {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.mcp-endpoint-label {
font-weight: 500;
color: var(--text-muted);
font-weight: 500;
color: var(--text-muted);
}
.mcp-endpoint-url {
font-family: var(--font-monospace);
padding: 6px 10px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
color: var(--text-accent);
user-select: text;
font-family: var(--font-monospace);
padding: 6px 10px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
color: var(--text-accent);
user-select: text;
}
.mcp-copy-endpoint-btn {
padding: 6px 12px;
background: var(--interactive-normal);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
button.mcp-copy-endpoint-btn {
padding: 6px 12px;
background: var(--interactive-normal);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
box-shadow: none;
}
.mcp-copy-endpoint-btn:hover {
background: var(--interactive-hover);
button.mcp-copy-endpoint-btn:hover {
background: var(--interactive-hover);
}
.mcp-copy-endpoint-btn.copied {
animation: copySuccess 0.3s ease;
background: var(--background-modifier-success) !important;
button.mcp-copy-endpoint-btn.copied {
animation: copySuccess 0.3s ease;
background: var(--background-modifier-success) !important;
}
/* Tools Grid */
.mcp-tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.mcp-tool-card {
position: relative;
padding: 16px;
background: linear-gradient(135deg, var(--background-secondary) 0%, var(--background-primary) 100%);
border: 1px solid var(--background-modifier-border);
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
padding: 16px;
background: linear-gradient(
135deg,
var(--background-secondary) 0%,
var(--background-primary) 100%
);
border: 1px solid var(--background-modifier-border);
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.mcp-tool-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--interactive-accent) 0%, var(--text-accent) 100%);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(
90deg,
var(--interactive-accent) 0%,
var(--text-accent) 100%
);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.mcp-tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: var(--interactive-accent);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: var(--interactive-accent);
}
.mcp-tool-card:hover::before {
transform: scaleX(1);
transform: scaleX(1);
}
.mcp-tool-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.mcp-tool-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--interactive-accent);
opacity: 0.9;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--interactive-accent);
opacity: 0.9;
}
.mcp-tool-icon svg {
width: 16px;
height: 16px;
stroke-width: 2;
width: 16px;
height: 16px;
stroke-width: 2;
}
.mcp-tool-name {
font-family: var(--font-monospace);
font-size: 0.85em;
font-weight: 600;
color: var(--text-accent);
background: var(--background-modifier-box);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
letter-spacing: 0.5px;
font-family: var(--font-monospace);
font-size: 0.85em;
font-weight: 600;
color: var(--text-accent);
background: var(--background-modifier-box);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
letter-spacing: 0.5px;
}
.mcp-tool-desc {
font-size: 0.85em;
color: var(--text-muted);
line-height: 1.5;
padding-left: 28px;
font-size: 0.85em;
color: var(--text-muted);
line-height: 1.5;
padding-left: 28px;
}
/* Example Container */
.mcp-example-container {
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
margin-top: 16px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
margin-top: 16px;
}
/* Example Tabs */
.mcp-example-tabs {
display: flex;
gap: 0;
background: linear-gradient(180deg, var(--background-secondary) 0%, var(--background-modifier-box) 100%);
border-bottom: 1px solid var(--background-modifier-border);
padding: 0;
display: flex;
gap: 0;
background: linear-gradient(
180deg,
var(--background-secondary) 0%,
var(--background-modifier-box) 100%
);
border-bottom: 1px solid var(--background-modifier-border);
padding: 0;
}
.mcp-example-tab {
flex: 1;
padding: 14px 20px;
background: transparent;
color: var(--text-muted);
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.9em;
font-weight: 500;
position: relative;
text-align: center;
box-shadow: none !important;
flex: 1;
padding: 14px 20px;
background: transparent;
color: var(--text-muted);
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.9em;
font-weight: 500;
position: relative;
text-align: center;
box-shadow: none !important;
}
.mcp-example-tab::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 80%;
height: 3px;
background: var(--interactive-accent);
transition: transform 0.3s ease;
border-radius: 3px 3px 0 0;
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 80%;
height: 3px;
background: var(--interactive-accent);
transition: transform 0.3s ease;
border-radius: 3px 3px 0 0;
}
.mcp-example-tab:hover {
background: rgba(var(--interactive-accent-rgb), 0.05);
color: var(--text-normal);
background: rgba(var(--interactive-accent-rgb), 0.05);
color: var(--text-normal);
}
.mcp-example-tab.active {
background: rgba(var(--interactive-accent-rgb), 0.1);
color: var(--interactive-accent);
font-weight: 600;
background: rgba(var(--interactive-accent-rgb), 0.1);
color: var(--interactive-accent);
font-weight: 600;
}
.mcp-example-tab.active::after {
transform: translateX(-50%) scaleX(1);
transform: translateX(-50%) scaleX(1);
}
/* Example Code */
.mcp-example-code-container {
position: relative;
background: var(--code-background);
border-top: 1px solid var(--background-modifier-border);
position: relative;
background: var(--code-background);
border-top: 1px solid var(--background-modifier-border);
}
.mcp-example-block {
display: none;
position: relative;
animation: fadeIn 0.3s ease;
display: none;
position: relative;
animation: fadeIn 0.3s ease;
}
.mcp-example-block.active {
display: block;
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.mcp-example-code {
margin: 0;
padding: 24px 80px 24px 24px;
background: linear-gradient(135deg, var(--code-background) 0%, rgba(var(--background-primary-rgb), 0.5) 100%);
font-size: 0.85em;
line-height: 1.7;
overflow-x: auto;
max-height: 500px;
font-family: var(--font-monospace);
border: none;
position: relative;
margin: 0;
padding: 24px 80px 24px 24px;
background: linear-gradient(
135deg,
var(--code-background) 0%,
rgba(var(--background-primary-rgb), 0.5) 100%
);
font-size: 0.85em;
line-height: 1.7;
overflow-x: auto;
max-height: 500px;
font-family: var(--font-monospace);
border: none;
position: relative;
}
.mcp-example-code::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--background-modifier-border), transparent);
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--background-modifier-border),
transparent
);
}
.mcp-example-code code {
color: var(--code-normal);
font-family: var(--font-monospace);
white-space: pre;
display: block;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
color: var(--code-normal);
font-family: var(--font-monospace);
white-space: pre;
display: block;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.mcp-example-copy-btn {
position: absolute;
top: 20px;
right: 20px;
padding: 8px 16px;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85em;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
box-shadow: 0 2px 8px rgba(var(--interactive-accent-rgb), 0.3);
position: absolute;
top: 20px;
right: 20px;
padding: 8px 16px;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.85em;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
box-shadow: 0 2px 8px rgba(var(--interactive-accent-rgb), 0.3);
}
.mcp-example-copy-btn:hover {
background: var(--interactive-accent-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--interactive-accent-rgb), 0.4);
background: var(--interactive-accent-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--interactive-accent-rgb), 0.4);
}
.mcp-example-copy-btn:active {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(var(--interactive-accent-rgb), 0.3);
transform: translateY(0);
box-shadow: 0 1px 4px rgba(var(--interactive-accent-rgb), 0.3);
}
.mcp-example-copy-btn.copied {
animation: copySuccess 0.4s ease;
background: var(--background-modifier-success) !important;
box-shadow: 0 2px 8px rgba(var(--interactive-success-rgb), 0.3);
animation: copySuccess 0.4s ease;
background: var(--background-modifier-success) !important;
box-shadow: 0 2px 8px rgba(var(--interactive-success-rgb), 0.3);
}
/* Animations */
@keyframes copySuccess {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
/* Smooth transitions for all interactive elements */
@ -540,40 +574,40 @@
.mcp-example-tab,
.mcp-tool-card,
button {
transition: all 0.2s ease;
transition: all 0.2s ease;
}
/* Code syntax colors for better readability */
.mcp-config-code .json-key {
color: var(--text-accent);
color: var(--text-accent);
}
.mcp-config-code .json-string {
color: var(--text-success);
color: var(--text-success);
}
.mcp-config-code .json-number {
color: var(--text-warning);
color: var(--text-warning);
}
.mcp-config-code .json-boolean {
color: var(--text-error);
color: var(--text-error);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.mcp-tools-grid {
grid-template-columns: 1fr;
}
.mcp-status-details {
flex-direction: column;
gap: 8px;
}
.mcp-tools-grid {
grid-template-columns: 1fr;
}
.mcp-status-details {
flex-direction: column;
gap: 8px;
}
}
.mcp-example-subtitle {
padding-left: var(--size-4-4);
padding-left: var(--size-4-4);
}
/* Cursor One-Click Install Section */
@ -607,7 +641,6 @@ button {
gap: 10px;
}
.mcp-cursor-copy-deeplink-btn {
background: var(--interactive-normal);
color: var(--text-normal);
@ -685,4 +718,4 @@ button {
margin: 24px 0;
border: none;
border-top: 1px solid var(--background-modifier-border);
}
}

View file

@ -126,3 +126,182 @@
color: var(--text-muted);
font-size: var(--font-ui-small);
}
/* MCP Log Modal Styles */
.mcp-log-modal {
min-width: 700px;
max-width: 900px;
}
.mcp-log-header {
display: flex;
align-items: center;
gap: var(--size-4-3);
margin-bottom: var(--size-4-3);
padding-bottom: var(--size-4-3);
border-bottom: 1px solid var(--background-modifier-border);
}
.mcp-log-search-container {
flex: 1;
}
.mcp-log-search-input {
width: 100%;
padding: var(--size-2-2) var(--size-4-2);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small);
}
.mcp-log-search-input:focus {
outline: none;
border-color: var(--interactive-accent);
}
.mcp-log-stats {
margin-bottom: var(--size-4-3);
padding: var(--size-2-3);
background: var(--background-secondary);
border-radius: var(--radius-s);
}
.mcp-log-stats-text {
font-size: var(--font-ui-small);
color: var(--text-muted);
text-align: center;
}
.mcp-log-container {
max-height: 500px;
overflow-y: auto;
padding: var(--size-2-2);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
}
.mcp-log-empty {
text-align: center;
padding: var(--size-4-8);
color: var(--text-muted);
font-style: italic;
}
.mcp-log-entry {
margin-bottom: var(--size-2-3);
padding: var(--size-2-3);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-secondary);
}
.mcp-log-entry:last-child {
margin-bottom: 0;
}
.mcp-log-error {
background: var(--background-modifier-error);
}
.mcp-log-entry-header {
display: flex;
align-items: center;
gap: var(--size-4-2);
font-size: var(--font-ui-small);
}
.mcp-log-timestamp {
color: var(--text-muted);
font-family: var(--font-monospace);
font-size: var(--font-smallest);
min-width: 80px;
}
.mcp-log-tool-name {
font-weight: 500;
color: var(--text-normal);
flex: 1;
}
.mcp-log-duration {
color: var(--text-muted);
font-family: var(--font-monospace);
font-size: var(--font-smallest);
}
.mcp-log-session-id {
color: var(--text-faint);
font-family: var(--font-monospace);
font-size: var(--font-smallest);
cursor: help;
}
.mcp-log-toggle-btn {
padding: 2px 6px;
border: none;
background: var(--interactive-normal);
color: var(--text-muted);
border-radius: var(--radius-s);
cursor: pointer;
font-size: var(--font-smallest);
transition: background-color 0.2s ease;
}
.mcp-log-toggle-btn:hover {
background: var(--interactive-hover);
}
.mcp-log-details {
margin-top: var(--size-2-3);
padding-top: var(--size-2-3);
border-top: 1px solid var(--background-modifier-border);
}
.mcp-log-section {
margin-bottom: var(--size-2-3);
}
.mcp-log-section:last-child {
margin-bottom: 0;
}
.mcp-log-section-title {
font-size: var(--font-ui-smaller);
font-weight: 600;
color: var(--text-normal);
margin-bottom: var(--size-2-1);
}
.mcp-log-error-title {
color: var(--text-error);
}
.mcp-log-section-content {
font-family: var(--font-monospace);
font-size: var(--font-smallest);
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-2-2);
margin: 0;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
}
.mcp-log-error-content {
color: var(--text-error);
background: var(--background-modifier-error);
}
.mcp-log-truncated-badge {
font-size: var(--font-smallest);
color: var(--text-muted);
font-weight: normal;
font-style: italic;
}

View file

@ -643,7 +643,8 @@ export function filterTasks(
if (filterRules.project) {
filtered = filtered.filter(
(task) =>
task.metadata.project?.trim() === filterRules.project?.trim(),
typeof task.metadata.project === 'string' && typeof filterRules.project === 'string' &&
task.metadata.project.trim() === filterRules.project.trim(),
);
}
if (filterRules.priority !== undefined) {

View file

@ -579,7 +579,7 @@ export function getEffectiveProject(task: Task): string | undefined {
}
// Check original project - must be non-empty and not just whitespace
if (task.metadata.project && task.metadata.project.trim()) {
if (task.metadata.project && typeof task.metadata.project === 'string' && task.metadata.project.trim()) {
return task.metadata.project;
}
@ -606,7 +606,7 @@ export function isProjectReadonly(task: Task): boolean {
}
// If there's an original project that's not empty/whitespace, it's always editable
if (task.metadata.project && task.metadata.project.trim()) {
if (task.metadata.project && typeof task.metadata.project === 'string' && task.metadata.project.trim()) {
return false;
}
@ -633,7 +633,7 @@ export function hasProject(task: Task): boolean {
}
// Check if original project exists and is not empty/whitespace
if (task.metadata.project && task.metadata.project.trim()) {
if (task.metadata.project && typeof task.metadata.project === 'string' && task.metadata.project.trim()) {
return true;
}

File diff suppressed because one or more lines are too long