feat: use structuredContent with outputSchema in example servers (#220)
* build app-with-deps
* make registerAppTool more interchangeable w/ server.registerTool
* feat: use structuredContent in all example servers
All servers now return structuredContent alongside JSON text content.
All mcp-apps now read structuredContent directly (no JSON.parse fallback).
Updated servers:
- basic-server-vanillajs
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- integration-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- video-resource-server
- wiki-explorer-server
* feat: add outputSchema to all example servers
Define output schemas for structuredContent validation:
- basic-server-vanillajs: { time: string }
- budget-allocator-server: BudgetDataResponseSchema
- cohort-heatmap-server: CohortDataSchema
- customer-segmentation-server: customers/segments schema
- integration-server: { time: string }
- scenario-modeler-server: templates/defaultInputs schema
- system-monitor-server: SystemStatsSchema
- threejs-server: { code, height }
- video-resource-server: { videoUri, description }
- wiki-explorer-server: { page, links, error }
* prettier:fix
* make registerAppTool more interchangeable w/ server.registerTool
* feat: use structuredContent in all example servers
All servers now return structuredContent alongside JSON text content.
All mcp-apps now read structuredContent directly (no JSON.parse fallback).
Updated servers:
- basic-server-vanillajs
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- integration-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- video-resource-server
- wiki-explorer-server
* feat: add outputSchema to all example servers
Define output schemas for structuredContent validation:
- basic-server-vanillajs: { time: string }
- budget-allocator-server: BudgetDataResponseSchema
- cohort-heatmap-server: CohortDataSchema
- customer-segmentation-server: customers/segments schema
- integration-server: { time: string }
- scenario-modeler-server: templates/defaultInputs schema
- system-monitor-server: SystemStatsSchema
- threejs-server: { code, height }
- video-resource-server: { videoUri, description }
- wiki-explorer-server: { page, links, error }
* prettier:fix
* revert: remove unrelated workflow, version, and arch changes
* chore: update package-lock.json
* refactor(basic-server-vanillajs): address PR review comments
- Simplify server.ts to return plain time string in text content (not JSON),
matching the quickstart guide pattern
- Add defensive coding in mcp-app.ts with optional chaining and fallback
to handle potential type mismatches gracefully
* refactor: restore rich text formatting for LLM-readable content
Restore the human-readable text formatting functions that were removed,
providing meaningful summaries in the text content while keeping
structuredContent for programmatic UI consumption:
- budget-allocator-server: formatBudgetSummary with config overview
- cohort-heatmap-server: formatCohortSummary with retention stats
- scenario-modeler-server: formatScenarioSummary with financial metrics
This commit is contained in:
parent
366aa9b66e
commit
9f09932baf
19 changed files with 219 additions and 124 deletions
|
|
@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "./server-utils.js";
|
||||
|
||||
|
|
@ -28,12 +29,16 @@ export function createServer(): McpServer {
|
|||
title: "Get Time",
|
||||
description: "Returns the current server time as an ISO 8601 string.",
|
||||
inputSchema: {},
|
||||
outputSchema: z.object({
|
||||
time: z.string(),
|
||||
}),
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
const time = new Date().toISOString();
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ time }) }],
|
||||
content: [{ type: "text", text: time }],
|
||||
structuredContent: { time },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ const log = {
|
|||
|
||||
|
||||
function extractTime(result: CallToolResult): string {
|
||||
const { text } = result.content?.find((c) => c.type === "text")!;
|
||||
return text;
|
||||
const { time } = (result.structuredContent as { time?: string }) ?? {};
|
||||
return time ?? "[ERROR]";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -224,6 +224,30 @@ function generateHistory(
|
|||
return months;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response Formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function formatBudgetSummary(data: BudgetDataResponse): string {
|
||||
const lines: string[] = [
|
||||
"Budget Allocator Configuration",
|
||||
"==============================",
|
||||
"",
|
||||
`Default Budget: ${data.config.currencySymbol}${data.config.defaultBudget.toLocaleString()}`,
|
||||
`Available Presets: ${data.config.presetBudgets.map((b) => `${data.config.currencySymbol}${b.toLocaleString()}`).join(", ")}`,
|
||||
"",
|
||||
"Categories:",
|
||||
...data.config.categories.map(
|
||||
(c) => ` - ${c.name}: ${c.defaultPercent}% default`,
|
||||
),
|
||||
"",
|
||||
`Historical Data: ${data.analytics.history.length} months`,
|
||||
`Benchmark Stages: ${data.analytics.stages.join(", ")}`,
|
||||
`Default Stage: ${data.analytics.defaultStage}`,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP Server Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -248,6 +272,7 @@ export function createServer(): McpServer {
|
|||
description:
|
||||
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
|
||||
inputSchema: {},
|
||||
outputSchema: BudgetDataResponseSchema,
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
|
|
@ -276,9 +301,10 @@ export function createServer(): McpServer {
|
|||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(response),
|
||||
text: formatBudgetSummary(response),
|
||||
},
|
||||
],
|
||||
structuredContent: response,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -607,13 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" });
|
|||
|
||||
app.ontoolresult = (result) => {
|
||||
log.info("Received tool result:", result);
|
||||
const text = result
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const data = JSON.parse(text) as BudgetDataResponse;
|
||||
const data = result.structuredContent as unknown as BudgetDataResponse;
|
||||
if (data?.config && data?.analytics) {
|
||||
initializeUI(data.config, data.analytics);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,17 @@ function generateCohortData(
|
|||
};
|
||||
}
|
||||
|
||||
function formatCohortSummary(data: CohortData): string {
|
||||
const avgRetention = data.cohorts
|
||||
.flatMap((c) => c.cells)
|
||||
.filter((cell) => cell.periodIndex > 0)
|
||||
.reduce((sum, cell, _, arr) => sum + cell.retention / arr.length, 0);
|
||||
|
||||
return `Cohort Analysis: ${data.cohorts.length} cohorts, ${data.periods.length} periods
|
||||
Average retention: ${(avgRetention * 100).toFixed(1)}%
|
||||
Metric: ${data.metric}, Period: ${data.periodType}`;
|
||||
}
|
||||
|
||||
export function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
name: "Cohort Heatmap Server",
|
||||
|
|
@ -169,6 +180,7 @@ export function createServer(): McpServer {
|
|||
description:
|
||||
"Returns cohort retention heatmap data showing customer retention over time by signup month",
|
||||
inputSchema: GetCohortDataInputSchema.shape,
|
||||
outputSchema: CohortDataSchema.shape,
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async ({ metric, periodType, cohortCount, maxPeriods }) => {
|
||||
|
|
@ -180,7 +192,8 @@ export function createServer(): McpServer {
|
|||
);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
||||
content: [{ type: "text", text: formatCohortSummary(data) }],
|
||||
structuredContent: data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -123,13 +123,7 @@ function CohortHeatmapInner({
|
|||
maxPeriods: 12,
|
||||
},
|
||||
});
|
||||
const text = result
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
setData(JSON.parse(text) as CohortData);
|
||||
setData(result.structuredContent as unknown as CohortData);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch cohort data:", e);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,29 @@ const GetCustomerDataInputSchema = z.object({
|
|||
.describe("Filter by segment (default: All)"),
|
||||
});
|
||||
|
||||
const CustomerSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
segment: z.string(),
|
||||
annualRevenue: z.number(),
|
||||
employeeCount: z.number(),
|
||||
accountAge: z.number(),
|
||||
engagementScore: z.number(),
|
||||
supportTickets: z.number(),
|
||||
nps: z.number(),
|
||||
});
|
||||
|
||||
const SegmentSummarySchema = z.object({
|
||||
name: z.string(),
|
||||
count: z.number(),
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
const GetCustomerDataOutputSchema = z.object({
|
||||
customers: z.array(CustomerSchema),
|
||||
segments: z.array(SegmentSummarySchema),
|
||||
});
|
||||
|
||||
// Cache generated data for session consistency
|
||||
let cachedCustomers: Customer[] | null = null;
|
||||
let cachedSegments: SegmentSummary[] | null = null;
|
||||
|
|
@ -78,6 +101,7 @@ export function createServer(): McpServer {
|
|||
description:
|
||||
"Returns customer data with segment information for visualization. Optionally filter by segment.",
|
||||
inputSchema: GetCustomerDataInputSchema.shape,
|
||||
outputSchema: GetCustomerDataOutputSchema.shape,
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async ({ segment }): Promise<CallToolResult> => {
|
||||
|
|
@ -85,6 +109,7 @@ export function createServer(): McpServer {
|
|||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
||||
structuredContent: data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
registerAppTool,
|
||||
registerAppResource,
|
||||
|
|
@ -33,16 +34,21 @@ export function createServer(): McpServer {
|
|||
title: "Get Time",
|
||||
description: "Returns the current server time.",
|
||||
inputSchema: {},
|
||||
outputSchema: z.object({
|
||||
time: z.string(),
|
||||
}),
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
const time = new Date().toISOString();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ time: new Date().toISOString() }),
|
||||
text: JSON.stringify({ time }),
|
||||
},
|
||||
],
|
||||
structuredContent: { time },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,13 +17,7 @@ const log = {
|
|||
};
|
||||
|
||||
function extractTime(callToolResult: CallToolResult): string {
|
||||
const text = callToolResult
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const { time } = JSON.parse(text) as { time: string };
|
||||
const { time } = callToolResult.structuredContent as { time: string };
|
||||
return time;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,13 @@ const GetScenarioDataInputSchema = z.object({
|
|||
),
|
||||
});
|
||||
|
||||
const GetScenarioDataOutputSchema = z.object({
|
||||
templates: z.array(ScenarioTemplateSchema),
|
||||
defaultInputs: ScenarioInputsSchema,
|
||||
customProjections: z.array(MonthlyProjectionSchema).optional(),
|
||||
customSummary: ScenarioSummarySchema.optional(),
|
||||
});
|
||||
|
||||
// Types derived from schemas
|
||||
type ScenarioInputs = z.infer<typeof ScenarioInputsSchema>;
|
||||
type MonthlyProjection = z.infer<typeof MonthlyProjectionSchema>;
|
||||
|
|
@ -243,6 +250,37 @@ const DEFAULT_INPUTS: ScenarioInputs = {
|
|||
fixedCosts: 30000,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Formatters for text output
|
||||
// ============================================================================
|
||||
|
||||
function formatCurrency(value: number): string {
|
||||
const absValue = Math.abs(value);
|
||||
const sign = value < 0 ? "-" : "";
|
||||
if (absValue >= 1_000_000) {
|
||||
return `${sign}$${(absValue / 1_000_000).toFixed(2)}M`;
|
||||
}
|
||||
if (absValue >= 1_000) {
|
||||
return `${sign}$${(absValue / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
return `${sign}$${Math.round(absValue)}`;
|
||||
}
|
||||
|
||||
function formatScenarioSummary(
|
||||
summary: ScenarioSummary,
|
||||
label: string,
|
||||
): string {
|
||||
return [
|
||||
`${label}:`,
|
||||
` Ending MRR: ${formatCurrency(summary.endingMRR)}`,
|
||||
` ARR: ${formatCurrency(summary.arr)}`,
|
||||
` Total Revenue: ${formatCurrency(summary.totalRevenue)}`,
|
||||
` Total Profit: ${formatCurrency(summary.totalProfit)}`,
|
||||
` MRR Growth: ${summary.mrrGrowthPct.toFixed(1)}%`,
|
||||
` Break-even: ${summary.breakEvenMonth ? `Month ${summary.breakEvenMonth}` : "Not achieved"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP Server
|
||||
// ============================================================================
|
||||
|
|
@ -269,6 +307,7 @@ export function createServer(): McpServer {
|
|||
description:
|
||||
"Returns SaaS scenario templates and optionally computes custom projections for given inputs",
|
||||
inputSchema: GetScenarioDataInputSchema.shape,
|
||||
outputSchema: GetScenarioDataOutputSchema.shape,
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async (args: {
|
||||
|
|
@ -278,18 +317,28 @@ export function createServer(): McpServer {
|
|||
? calculateScenario(args.customInputs)
|
||||
: undefined;
|
||||
|
||||
const text = [
|
||||
"SaaS Scenario Modeler",
|
||||
"=".repeat(40),
|
||||
"",
|
||||
"Available Templates:",
|
||||
...SCENARIO_TEMPLATES.map(
|
||||
(t) => ` ${t.icon} ${t.name}: ${t.description}`,
|
||||
),
|
||||
"",
|
||||
customScenario
|
||||
? formatScenarioSummary(customScenario.summary, "Custom Scenario")
|
||||
: "Use customInputs parameter to compute projections for a specific scenario.",
|
||||
].join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
templates: SCENARIO_TEMPLATES,
|
||||
defaultInputs: DEFAULT_INPUTS,
|
||||
customProjections: customScenario?.projections,
|
||||
customSummary: customScenario?.summary,
|
||||
}),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text }],
|
||||
structuredContent: {
|
||||
templates: SCENARIO_TEMPLATES,
|
||||
defaultInputs: DEFAULT_INPUTS,
|
||||
customProjections: customScenario?.projections,
|
||||
customSummary: customScenario?.summary,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
||||
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { StrictMode, useState, useMemo, useCallback, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { SliderRow } from "./components/SliderRow.tsx";
|
||||
|
|
@ -25,18 +24,6 @@ interface CallToolResultData {
|
|||
defaultInputs?: ScenarioInputs;
|
||||
}
|
||||
|
||||
/** Extract templates and defaultInputs from tool result content */
|
||||
function extractResultData(result: CallToolResult): CallToolResultData {
|
||||
const text = result
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const { templates, defaultInputs } = JSON.parse(text) as CallToolResultData;
|
||||
return { templates, defaultInputs };
|
||||
}
|
||||
|
||||
const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" };
|
||||
|
||||
function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) {
|
||||
|
|
@ -70,7 +57,8 @@ function ScenarioModeler() {
|
|||
capabilities: {},
|
||||
onAppCreated: (app) => {
|
||||
app.ontoolresult = async (result) => {
|
||||
const { templates, defaultInputs } = extractResultData(result);
|
||||
const { templates, defaultInputs } =
|
||||
result.structuredContent as unknown as CallToolResultData;
|
||||
if (templates) setTemplates(templates);
|
||||
if (defaultInputs) setDefaultInputs(defaultInputs);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export function createServer(): McpServer {
|
|||
description:
|
||||
"Returns current system statistics including per-core CPU usage, memory, and system info.",
|
||||
inputSchema: {},
|
||||
outputSchema: SystemStatsSchema.shape,
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
|
|
@ -151,6 +152,7 @@ export function createServer(): McpServer {
|
|||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(stats) }],
|
||||
structuredContent: stats,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -265,13 +265,7 @@ async function fetchStats(): Promise<void> {
|
|||
arguments: {},
|
||||
});
|
||||
|
||||
const text = result
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const stats = JSON.parse(text) as SystemStats;
|
||||
const stats = result.structuredContent as unknown as SystemStats;
|
||||
|
||||
// Initialize chart on first data if needed
|
||||
if (!state.chart && stats.cpu.count > 0) {
|
||||
|
|
|
|||
|
|
@ -166,16 +166,17 @@ export function createServer(): McpServer {
|
|||
.default(400)
|
||||
.describe("Height in pixels"),
|
||||
},
|
||||
outputSchema: z.object({
|
||||
code: z.string(),
|
||||
height: z.number(),
|
||||
}),
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async ({ code, height }) => {
|
||||
const data = { code, height };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ code, height }),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
||||
structuredContent: data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -129,20 +129,21 @@ ${Object.entries(VIDEO_LIBRARY)
|
|||
`Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
|
||||
),
|
||||
},
|
||||
outputSchema: z.object({
|
||||
videoUri: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
},
|
||||
async ({ videoId }): Promise<CallToolResult> => {
|
||||
const video = VIDEO_LIBRARY[videoId];
|
||||
const data = {
|
||||
videoUri: `videos://${videoId}`,
|
||||
description: video.description,
|
||||
};
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
videoUri: `videos://${videoId}`,
|
||||
description: video.description,
|
||||
}),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
||||
structuredContent: data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,18 +28,10 @@ const videoInfoEl = document.getElementById("video-info")!;
|
|||
function parseToolResult(
|
||||
result: CallToolResult,
|
||||
): { videoUri: string; description: string } | null {
|
||||
const text = result.content
|
||||
?.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(text) as { videoUri: string; description: string };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return result.structuredContent as {
|
||||
videoUri: string;
|
||||
description: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// Show states
|
||||
|
|
|
|||
|
|
@ -95,6 +95,19 @@ export function createServer(): McpServer {
|
|||
.default("https://en.wikipedia.org/wiki/Model_Context_Protocol")
|
||||
.describe("Wikipedia page URL"),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
page: z.object({
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
links: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async ({ url }): Promise<CallToolResult> => {
|
||||
|
|
@ -121,11 +134,17 @@ export function createServer(): McpServer {
|
|||
const links = extractWikiLinks(new URL(url), html);
|
||||
|
||||
const result = { page: { url, title }, links, error: null };
|
||||
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result) }],
|
||||
structuredContent: result,
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const result = { page: { url, title }, links: [], error };
|
||||
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result) }],
|
||||
structuredContent: result,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -322,44 +322,36 @@ app.ontoolresult = (result) => {
|
|||
};
|
||||
|
||||
function handleToolResultData(result: CallToolResult): void {
|
||||
if (
|
||||
result.isError ||
|
||||
!result.content?.[0] ||
|
||||
result.content[0].type !== "text"
|
||||
) {
|
||||
if (result.isError) {
|
||||
console.error("Tool result error:", result);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response: ToolResponse = JSON.parse(result.content[0].text);
|
||||
const { page, links, error } = response;
|
||||
const response = result.structuredContent as unknown as ToolResponse;
|
||||
const { page, links, error } = response;
|
||||
|
||||
// Ensure the source node exists
|
||||
addNode(page.url, page.title);
|
||||
updateNodeTitle(page.url, page.title);
|
||||
// Ensure the source node exists
|
||||
addNode(page.url, page.title);
|
||||
updateNodeTitle(page.url, page.title);
|
||||
|
||||
if (error) {
|
||||
setNodeState(page.url, "error", error);
|
||||
} else {
|
||||
// Get source node position so new nodes appear nearby
|
||||
const sourceNode = graphData.nodes.find((n) => n.url === page.url);
|
||||
const sourcePos = sourceNode
|
||||
? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 }
|
||||
: undefined;
|
||||
if (error) {
|
||||
setNodeState(page.url, "error", error);
|
||||
} else {
|
||||
// Get source node position so new nodes appear nearby
|
||||
const sourceNode = graphData.nodes.find((n) => n.url === page.url);
|
||||
const sourcePos = sourceNode
|
||||
? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 }
|
||||
: undefined;
|
||||
|
||||
// Add all linked nodes and edges
|
||||
for (const link of links) {
|
||||
addNode(link.url, link.title, "default", sourcePos);
|
||||
addEdge(page.url, link.url);
|
||||
}
|
||||
setNodeState(page.url, "expanded");
|
||||
// Add all linked nodes and edges
|
||||
for (const link of links) {
|
||||
addNode(link.url, link.title, "default", sourcePos);
|
||||
addEdge(page.url, link.url);
|
||||
}
|
||||
|
||||
updateGraph();
|
||||
} catch (e) {
|
||||
console.error("Failed to parse tool result:", e);
|
||||
setNodeState(page.url, "expanded");
|
||||
}
|
||||
|
||||
updateGraph();
|
||||
}
|
||||
|
||||
app.onerror = (err) => {
|
||||
|
|
|
|||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -1635,9 +1635,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
||||
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
||||
"version": "1.19.8",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz",
|
||||
"integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
|
|
@ -3358,9 +3358,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.13",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz",
|
||||
"integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==",
|
||||
"version": "2.9.14",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
|
||||
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue