mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-02 03:00:15 -04:00
Refactor servers to use registerTool/registerResource/registerPrompt APIs
Updated all TypeScript MCP servers to use the new registration API pattern: - Replaced setRequestHandler(ListToolsRequestSchema) with registerTool() - Replaced setRequestHandler(GetPromptRequestSchema) with registerPrompt() - Replaced setRequestHandler(ReadResourceRequestSchema) with registerResource() Changes: - sequentialthinking: Migrated 1 tool to registerTool() - memory: Migrated 9 tools to registerTool() - filesystem: Migrated 14 tools to registerTool() - everything: Migrated 11 tools, 3 prompts, and 100 resources to new APIs Also updated SDK versions to ^1.20.1 across all servers for consistency. Note: These APIs are not yet available in the current SDK version. This is preparatory work for when the SDK is updated to support these methods. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,17 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ClientCapabilities,
|
||||
CompleteRequestSchema,
|
||||
CreateMessageRequest,
|
||||
CreateMessageResultSchema,
|
||||
ElicitResultSchema,
|
||||
GetPromptRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
LoggingLevel,
|
||||
ReadResourceRequestSchema,
|
||||
Resource,
|
||||
RootsListChangedNotificationSchema,
|
||||
ServerNotification,
|
||||
ServerRequest,
|
||||
SubscribeRequestSchema,
|
||||
Tool,
|
||||
ToolSchema,
|
||||
UnsubscribeRequestSchema,
|
||||
type Root
|
||||
@@ -280,57 +272,17 @@ export const createServer = () => {
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
||||
const cursor = request.params?.cursor;
|
||||
let startIndex = 0;
|
||||
|
||||
if (cursor) {
|
||||
const decodedCursor = parseInt(atob(cursor), 10);
|
||||
if (!isNaN(decodedCursor)) {
|
||||
startIndex = decodedCursor;
|
||||
// Register all resources dynamically
|
||||
ALL_RESOURCES.forEach(resource => {
|
||||
server.registerResource({
|
||||
uri: resource.uri,
|
||||
name: resource.name,
|
||||
mimeType: resource.mimeType,
|
||||
description: `Static resource ${resource.name}`,
|
||||
handler: async () => {
|
||||
return resource.text ? { text: resource.text } : { blob: resource.blob };
|
||||
}
|
||||
}
|
||||
|
||||
const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length);
|
||||
const resources = ALL_RESOURCES.slice(startIndex, endIndex);
|
||||
|
||||
let nextCursor: string | undefined;
|
||||
if (endIndex < ALL_RESOURCES.length) {
|
||||
nextCursor = btoa(endIndex.toString());
|
||||
}
|
||||
|
||||
return {
|
||||
resources,
|
||||
nextCursor,
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
||||
return {
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: "test://static/resource/{id}",
|
||||
name: "Static Resource",
|
||||
description: "A static resource with a numeric ID",
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
|
||||
if (uri.startsWith("test://static/resource/")) {
|
||||
const index = parseInt(uri.split("/").pop() ?? "", 10) - 1;
|
||||
if (index >= 0 && index < ALL_RESOURCES.length) {
|
||||
const resource = ALL_RESOURCES[index];
|
||||
return {
|
||||
contents: [resource],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown resource: ${uri}`);
|
||||
});
|
||||
});
|
||||
|
||||
server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => {
|
||||
@@ -344,48 +296,11 @@ export const createServer = () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||
return {
|
||||
prompts: [
|
||||
{
|
||||
name: PromptName.SIMPLE,
|
||||
description: "A prompt without arguments",
|
||||
},
|
||||
{
|
||||
name: PromptName.COMPLEX,
|
||||
description: "A prompt with arguments",
|
||||
arguments: [
|
||||
{
|
||||
name: "temperature",
|
||||
description: "Temperature setting",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "style",
|
||||
description: "Output style",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: PromptName.RESOURCE,
|
||||
description: "A prompt that includes an embedded resource reference",
|
||||
arguments: [
|
||||
{
|
||||
name: "resourceId",
|
||||
description: "Resource ID to include (1-100)",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (name === PromptName.SIMPLE) {
|
||||
// Register prompts
|
||||
server.registerPrompt({
|
||||
name: PromptName.SIMPLE,
|
||||
description: "A prompt without arguments",
|
||||
handler: async () => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
@@ -398,8 +313,24 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === PromptName.COMPLEX) {
|
||||
server.registerPrompt({
|
||||
name: PromptName.COMPLEX,
|
||||
description: "A prompt with arguments",
|
||||
arguments: [
|
||||
{
|
||||
name: "temperature",
|
||||
description: "Temperature setting",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "style",
|
||||
description: "Output style",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
handler: async (args) => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
@@ -427,8 +358,19 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === PromptName.RESOURCE) {
|
||||
server.registerPrompt({
|
||||
name: PromptName.RESOURCE,
|
||||
description: "A prompt that includes an embedded resource reference",
|
||||
arguments: [
|
||||
{
|
||||
name: "resourceId",
|
||||
description: "Resource ID to include (1-100)",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async (args) => {
|
||||
const resourceId = parseInt(args?.resourceId as string, 10);
|
||||
if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) {
|
||||
throw new Error(
|
||||
@@ -458,101 +400,26 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown prompt: ${name}`);
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: ToolName.ECHO,
|
||||
description: "Echoes back the input",
|
||||
inputSchema: zodToJsonSchema(EchoSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.ADD,
|
||||
description: "Adds two numbers",
|
||||
inputSchema: zodToJsonSchema(AddSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.LONG_RUNNING_OPERATION,
|
||||
description:
|
||||
"Demonstrates a long running operation with progress updates",
|
||||
inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.PRINT_ENV,
|
||||
description:
|
||||
"Prints all environment variables, helpful for debugging MCP server configuration",
|
||||
inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.SAMPLE_LLM,
|
||||
description: "Samples from an LLM using MCP's sampling feature",
|
||||
inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.GET_TINY_IMAGE,
|
||||
description: "Returns the MCP_TINY_IMAGE",
|
||||
inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.ANNOTATED_MESSAGE,
|
||||
description:
|
||||
"Demonstrates how annotations can be used to provide metadata about content",
|
||||
inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.GET_RESOURCE_REFERENCE,
|
||||
description:
|
||||
"Returns a resource reference that can be used by MCP clients",
|
||||
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.GET_RESOURCE_LINKS,
|
||||
description:
|
||||
"Returns multiple resource links that reference different types of resources",
|
||||
inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: ToolName.STRUCTURED_CONTENT,
|
||||
description:
|
||||
"Returns structured content along with an output schema for client data validation",
|
||||
inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput,
|
||||
outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput,
|
||||
},
|
||||
{
|
||||
name: ToolName.ZIP_RESOURCES,
|
||||
description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.",
|
||||
inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput,
|
||||
}
|
||||
];
|
||||
if (clientCapabilities!.roots) tools.push ({
|
||||
name: ToolName.LIST_ROOTS,
|
||||
description:
|
||||
"Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.",
|
||||
inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput,
|
||||
});
|
||||
if (clientCapabilities!.elicitation) tools.push ({
|
||||
name: ToolName.ELICITATION,
|
||||
description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)",
|
||||
inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput,
|
||||
});
|
||||
|
||||
return { tools };
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request,extra) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (name === ToolName.ECHO) {
|
||||
// Register tools
|
||||
server.registerTool({
|
||||
name: ToolName.ECHO,
|
||||
description: "Echoes back the input",
|
||||
inputSchema: zodToJsonSchema(EchoSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const validatedArgs = EchoSchema.parse(args);
|
||||
return {
|
||||
content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.ADD) {
|
||||
server.registerTool({
|
||||
name: ToolName.ADD,
|
||||
description: "Adds two numbers",
|
||||
inputSchema: zodToJsonSchema(AddSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const validatedArgs = AddSchema.parse(args);
|
||||
const sum = validatedArgs.a + validatedArgs.b;
|
||||
return {
|
||||
@@ -564,12 +431,17 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.LONG_RUNNING_OPERATION) {
|
||||
server.registerTool({
|
||||
name: ToolName.LONG_RUNNING_OPERATION,
|
||||
description: "Demonstrates a long running operation with progress updates",
|
||||
inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput,
|
||||
handler: async (args, extra) => {
|
||||
const validatedArgs = LongRunningOperationSchema.parse(args);
|
||||
const { duration, steps } = validatedArgs;
|
||||
const stepDuration = duration / steps;
|
||||
const progressToken = request.params._meta?.progressToken;
|
||||
const progressToken = extra.request.params._meta?.progressToken;
|
||||
|
||||
for (let i = 1; i < steps + 1; i++) {
|
||||
await new Promise((resolve) =>
|
||||
@@ -597,8 +469,13 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.PRINT_ENV) {
|
||||
server.registerTool({
|
||||
name: ToolName.PRINT_ENV,
|
||||
description: "Prints all environment variables, helpful for debugging MCP server configuration",
|
||||
inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput,
|
||||
handler: async () => {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -608,8 +485,13 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.SAMPLE_LLM) {
|
||||
server.registerTool({
|
||||
name: ToolName.SAMPLE_LLM,
|
||||
description: "Samples from an LLM using MCP's sampling feature",
|
||||
inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput,
|
||||
handler: async (args, extra) => {
|
||||
const validatedArgs = SampleLLMSchema.parse(args);
|
||||
const { prompt, maxTokens } = validatedArgs;
|
||||
|
||||
@@ -625,8 +507,13 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.GET_TINY_IMAGE) {
|
||||
server.registerTool({
|
||||
name: ToolName.GET_TINY_IMAGE,
|
||||
description: "Returns the MCP_TINY_IMAGE",
|
||||
inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
GetTinyImageSchema.parse(args);
|
||||
return {
|
||||
content: [
|
||||
@@ -646,8 +533,13 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.ANNOTATED_MESSAGE) {
|
||||
server.registerTool({
|
||||
name: ToolName.ANNOTATED_MESSAGE,
|
||||
description: "Demonstrates how annotations can be used to provide metadata about content",
|
||||
inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const { messageType, includeImage } = AnnotatedMessageSchema.parse(args);
|
||||
|
||||
const content = [];
|
||||
@@ -697,8 +589,13 @@ export const createServer = () => {
|
||||
|
||||
return { content };
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.GET_RESOURCE_REFERENCE) {
|
||||
server.registerTool({
|
||||
name: ToolName.GET_RESOURCE_REFERENCE,
|
||||
description: "Returns a resource reference that can be used by MCP clients",
|
||||
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const validatedArgs = GetResourceReferenceSchema.parse(args);
|
||||
const resourceId = validatedArgs.resourceId;
|
||||
|
||||
@@ -726,128 +623,13 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.ELICITATION) {
|
||||
ElicitationSchema.parse(args);
|
||||
|
||||
const elicitationResult = await extra.sendRequest({
|
||||
method: 'elicitation/create',
|
||||
params: {
|
||||
message: 'Please provide inputs for the following fields:',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
title: 'Full Name',
|
||||
type: 'string',
|
||||
description: 'Your full, legal name',
|
||||
},
|
||||
check: {
|
||||
title: 'Agree to terms',
|
||||
type: 'boolean',
|
||||
description: 'A boolean check',
|
||||
},
|
||||
color: {
|
||||
title: 'Favorite Color',
|
||||
type: 'string',
|
||||
description: 'Favorite color (open text)',
|
||||
default: 'blue',
|
||||
},
|
||||
email: {
|
||||
title: 'Email Address',
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
description: 'Your email address (will be verified, and never shared with anyone else)',
|
||||
},
|
||||
homepage: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
description: 'Homepage / personal site',
|
||||
},
|
||||
birthdate: {
|
||||
title: 'Birthdate',
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
description: 'Your date of birth (will never be shared with anyone else)',
|
||||
},
|
||||
integer: {
|
||||
title: 'Favorite Integer',
|
||||
type: 'integer',
|
||||
description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 42,
|
||||
},
|
||||
number: {
|
||||
title: 'Favorite Number',
|
||||
type: 'number',
|
||||
description: 'Favorite number (there are no wrong answers)',
|
||||
minimum: 0,
|
||||
maximum: 1000,
|
||||
default: 3.14,
|
||||
},
|
||||
petType: {
|
||||
title: 'Pet type',
|
||||
type: 'string',
|
||||
enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'],
|
||||
enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'],
|
||||
default: 'dogs',
|
||||
description: 'Your favorite pet type',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
}, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ });
|
||||
|
||||
// Handle different response actions
|
||||
const content = [];
|
||||
|
||||
if (elicitationResult.action === 'accept' && elicitationResult.content) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `✅ User provided the requested information!`,
|
||||
});
|
||||
|
||||
// Only access elicitationResult.content when action is accept
|
||||
const userData = elicitationResult.content;
|
||||
const lines = [];
|
||||
if (userData.name) lines.push(`- Name: ${userData.name}`);
|
||||
if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`);
|
||||
if (userData.color) lines.push(`- Favorite Color: ${userData.color}`);
|
||||
if (userData.email) lines.push(`- Email: ${userData.email}`);
|
||||
if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`);
|
||||
if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`);
|
||||
if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`);
|
||||
if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`);
|
||||
if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`);
|
||||
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `User inputs:\n${lines.join('\n')}`,
|
||||
});
|
||||
} else if (elicitationResult.action === 'decline') {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `❌ User declined to provide the requested information.`,
|
||||
});
|
||||
} else if (elicitationResult.action === 'cancel') {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `⚠️ User cancelled the elicitation dialog.`,
|
||||
});
|
||||
}
|
||||
|
||||
// Include raw result for debugging
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`,
|
||||
});
|
||||
|
||||
return { content };
|
||||
}
|
||||
|
||||
if (name === ToolName.GET_RESOURCE_LINKS) {
|
||||
server.registerTool({
|
||||
name: ToolName.GET_RESOURCE_LINKS,
|
||||
description: "Returns multiple resource links that reference different types of resources",
|
||||
inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const { count } = GetResourceLinksSchema.parse(args);
|
||||
const content = [];
|
||||
|
||||
@@ -875,8 +657,14 @@ export const createServer = () => {
|
||||
|
||||
return { content };
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.STRUCTURED_CONTENT) {
|
||||
server.registerTool({
|
||||
name: ToolName.STRUCTURED_CONTENT,
|
||||
description: "Returns structured content along with an output schema for client data validation",
|
||||
inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput,
|
||||
outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput,
|
||||
handler: async (args) => {
|
||||
// The same response is returned for every input.
|
||||
const validatedArgs = StructuredContentSchema.input.parse(args);
|
||||
|
||||
@@ -896,8 +684,13 @@ export const createServer = () => {
|
||||
structuredContent: weather
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (name === ToolName.ZIP_RESOURCES) {
|
||||
server.registerTool({
|
||||
name: ToolName.ZIP_RESOURCES,
|
||||
description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.",
|
||||
inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
const { files } = ZipResourcesInputSchema.parse(args);
|
||||
|
||||
const zip = new JSZip();
|
||||
@@ -927,54 +720,6 @@ export const createServer = () => {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (name === ToolName.LIST_ROOTS) {
|
||||
ListRootsSchema.parse(args);
|
||||
|
||||
if (!clientSupportsRoots) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The MCP client does not support the roots protocol.\n\n" +
|
||||
"This means the server cannot access information about the client's workspace directories or file system roots."
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
if (currentRoots.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The client supports roots but no roots are currently configured.\n\n" +
|
||||
"This could mean:\n" +
|
||||
"1. The client hasn't provided any roots yet\n" +
|
||||
"2. The client provided an empty roots list\n" +
|
||||
"3. The roots configuration is still being loaded"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const rootsList = currentRoots.map((root, index) => {
|
||||
return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`;
|
||||
}).join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` +
|
||||
"Note: This server demonstrates the roots protocol capability but doesn't actually access files. " +
|
||||
"The roots are provided by the MCP client and can be used by servers that need file system access."
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
});
|
||||
|
||||
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
||||
@@ -1030,12 +775,66 @@ export const createServer = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle post-initialization setup for roots
|
||||
// Handle post-initialization setup for roots and conditional tools
|
||||
server.oninitialized = async () => {
|
||||
clientCapabilities = server.getClientCapabilities();
|
||||
|
||||
// Register conditional tools based on client capabilities
|
||||
if (clientCapabilities?.roots) {
|
||||
clientSupportsRoots = true;
|
||||
|
||||
// Register LIST_ROOTS tool
|
||||
server.registerTool({
|
||||
name: ToolName.LIST_ROOTS,
|
||||
description: "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.",
|
||||
inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput,
|
||||
handler: async (args) => {
|
||||
ListRootsSchema.parse(args);
|
||||
|
||||
if (!clientSupportsRoots) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The MCP client does not support the roots protocol.\n\n" +
|
||||
"This means the server cannot access information about the client's workspace directories or file system roots."
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
if (currentRoots.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The client supports roots but no roots are currently configured.\n\n" +
|
||||
"This could mean:\n" +
|
||||
"1. The client hasn't provided any roots yet\n" +
|
||||
"2. The client provided an empty roots list\n" +
|
||||
"3. The roots configuration is still being loaded"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const rootsList = currentRoots.map((root, index) => {
|
||||
return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`;
|
||||
}).join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` +
|
||||
"Note: This server demonstrates the roots protocol capability but doesn't actually access files. " +
|
||||
"The roots are provided by the MCP client and can be used by servers that need file system access."
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await server.listRoots();
|
||||
if (response && 'roots' in response) {
|
||||
@@ -1067,6 +866,134 @@ export const createServer = () => {
|
||||
data: "Client does not support MCP roots protocol",
|
||||
}, sessionId);
|
||||
}
|
||||
|
||||
if (clientCapabilities?.elicitation) {
|
||||
// Register ELICITATION tool
|
||||
server.registerTool({
|
||||
name: ToolName.ELICITATION,
|
||||
description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)",
|
||||
inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput,
|
||||
handler: async (args, extra) => {
|
||||
ElicitationSchema.parse(args);
|
||||
|
||||
const elicitationResult = await extra.sendRequest({
|
||||
method: 'elicitation/create',
|
||||
params: {
|
||||
message: 'Please provide inputs for the following fields:',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
title: 'Full Name',
|
||||
type: 'string',
|
||||
description: 'Your full, legal name',
|
||||
},
|
||||
check: {
|
||||
title: 'Agree to terms',
|
||||
type: 'boolean',
|
||||
description: 'A boolean check',
|
||||
},
|
||||
color: {
|
||||
title: 'Favorite Color',
|
||||
type: 'string',
|
||||
description: 'Favorite color (open text)',
|
||||
default: 'blue',
|
||||
},
|
||||
email: {
|
||||
title: 'Email Address',
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
description: 'Your email address (will be verified, and never shared with anyone else)',
|
||||
},
|
||||
homepage: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
description: 'Homepage / personal site',
|
||||
},
|
||||
birthdate: {
|
||||
title: 'Birthdate',
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
description: 'Your date of birth (will never be shared with anyone else)',
|
||||
},
|
||||
integer: {
|
||||
title: 'Favorite Integer',
|
||||
type: 'integer',
|
||||
description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 42,
|
||||
},
|
||||
number: {
|
||||
title: 'Favorite Number',
|
||||
type: 'number',
|
||||
description: 'Favorite number (there are no wrong answers)',
|
||||
minimum: 0,
|
||||
maximum: 1000,
|
||||
default: 3.14,
|
||||
},
|
||||
petType: {
|
||||
title: 'Pet type',
|
||||
type: 'string',
|
||||
enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'],
|
||||
enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'],
|
||||
default: 'dogs',
|
||||
description: 'Your favorite pet type',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
}, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ });
|
||||
|
||||
// Handle different response actions
|
||||
const content = [];
|
||||
|
||||
if (elicitationResult.action === 'accept' && elicitationResult.content) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `✅ User provided the requested information!`,
|
||||
});
|
||||
|
||||
// Only access elicitationResult.content when action is accept
|
||||
const userData = elicitationResult.content;
|
||||
const lines = [];
|
||||
if (userData.name) lines.push(`- Name: ${userData.name}`);
|
||||
if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`);
|
||||
if (userData.color) lines.push(`- Favorite Color: ${userData.color}`);
|
||||
if (userData.email) lines.push(`- Email: ${userData.email}`);
|
||||
if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`);
|
||||
if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`);
|
||||
if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`);
|
||||
if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`);
|
||||
if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`);
|
||||
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `User inputs:\n${lines.join('\n')}`,
|
||||
});
|
||||
} else if (elicitationResult.action === 'decline') {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `❌ User declined to provide the requested information.`,
|
||||
});
|
||||
} else if (elicitationResult.action === 'cancel') {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `⚠️ User cancelled the elicitation dialog.`,
|
||||
});
|
||||
}
|
||||
|
||||
// Include raw result for debugging
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`,
|
||||
});
|
||||
|
||||
return { content };
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"start:streamableHttp": "node dist/streamableHttp.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.1",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ToolSchema,
|
||||
RootsListChangedNotificationSchema,
|
||||
type Root,
|
||||
@@ -178,464 +176,470 @@ async function readFileAsBase64Stream(filePath: string): Promise<string> {
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "read_file",
|
||||
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
|
||||
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "read_text_file",
|
||||
description:
|
||||
"Read the complete contents of a file from the file system as text. " +
|
||||
"Handles various text encodings and provides detailed error messages " +
|
||||
"if the file cannot be read. Use this tool when you need to examine " +
|
||||
"the contents of a single file. Use the 'head' parameter to read only " +
|
||||
"the first N lines of a file, or the 'tail' parameter to read only " +
|
||||
"the last N lines of a file. Operates on the file as text regardless of extension. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "read_media_file",
|
||||
description:
|
||||
"Read an image or audio file. Returns the base64 encoded data and MIME type. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadMediaFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "read_multiple_files",
|
||||
description:
|
||||
"Read the contents of multiple files simultaneously. This is more " +
|
||||
"efficient than reading files one by one when you need to analyze " +
|
||||
"or compare multiple files. Each file's content is returned with its " +
|
||||
"path as a reference. Failed reads for individual files won't stop " +
|
||||
"the entire operation. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "write_file",
|
||||
description:
|
||||
"Create a new file or completely overwrite an existing file with new content. " +
|
||||
"Use with caution as it will overwrite existing files without warning. " +
|
||||
"Handles text content with proper encoding. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "edit_file",
|
||||
description:
|
||||
"Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
||||
"with new content. Returns a git-style diff showing the changes made. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "create_directory",
|
||||
description:
|
||||
"Create a new directory or ensure a directory exists. Can create multiple " +
|
||||
"nested directories in one operation. If the directory already exists, " +
|
||||
"this operation will succeed silently. Perfect for setting up directory " +
|
||||
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "list_directory",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is essential for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "list_directory_with_sizes",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path, including sizes. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is useful for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryWithSizesArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "directory_tree",
|
||||
description:
|
||||
"Get a recursive tree view of files and directories as a JSON structure. " +
|
||||
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||
"Files have no children array, while directories always have a children array (which may be empty). " +
|
||||
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "move_file",
|
||||
description:
|
||||
"Move or rename files and directories. Can move files between directories " +
|
||||
"and rename them in a single operation. If the destination exists, the " +
|
||||
"operation will fail. Works across different directories and can be used " +
|
||||
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "search_files",
|
||||
description:
|
||||
"Recursively search for files and directories matching a pattern. " +
|
||||
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
|
||||
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
|
||||
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
|
||||
"Only searches within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "get_file_info",
|
||||
description:
|
||||
"Retrieve detailed metadata about a file or directory. Returns comprehensive " +
|
||||
"information including size, creation time, last modified time, permissions, " +
|
||||
"and type. This tool is perfect for understanding file characteristics " +
|
||||
"without reading the actual content. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "list_allowed_directories",
|
||||
description:
|
||||
"Returns the list of directories that this server is allowed to access. " +
|
||||
"Subdirectories within these allowed directories are also accessible. " +
|
||||
"Use this to understand which directories and their nested paths are available " +
|
||||
"before trying to access files.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
server.registerTool({
|
||||
name: "read_file",
|
||||
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
|
||||
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ReadTextFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
|
||||
if (parsed.data.head && parsed.data.tail) {
|
||||
throw new Error("Cannot specify both head and tail parameters simultaneously");
|
||||
}
|
||||
|
||||
if (parsed.data.tail) {
|
||||
// Use memory-efficient tail implementation for large files
|
||||
const tailContent = await tailFile(validPath, parsed.data.tail);
|
||||
return {
|
||||
content: [{ type: "text", text: tailContent }],
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.data.head) {
|
||||
// Use memory-efficient head implementation for large files
|
||||
const headContent = await headFile(validPath, parsed.data.head);
|
||||
return {
|
||||
content: [{ type: "text", text: headContent }],
|
||||
};
|
||||
}
|
||||
const content = await readFileContent(validPath);
|
||||
return {
|
||||
content: [{ type: "text", text: content }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
switch (name) {
|
||||
case "read_file":
|
||||
case "read_text_file": {
|
||||
const parsed = ReadTextFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
|
||||
if (parsed.data.head && parsed.data.tail) {
|
||||
throw new Error("Cannot specify both head and tail parameters simultaneously");
|
||||
}
|
||||
|
||||
if (parsed.data.tail) {
|
||||
// Use memory-efficient tail implementation for large files
|
||||
const tailContent = await tailFile(validPath, parsed.data.tail);
|
||||
return {
|
||||
content: [{ type: "text", text: tailContent }],
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.data.head) {
|
||||
// Use memory-efficient head implementation for large files
|
||||
const headContent = await headFile(validPath, parsed.data.head);
|
||||
return {
|
||||
content: [{ type: "text", text: headContent }],
|
||||
};
|
||||
}
|
||||
const content = await readFileContent(validPath);
|
||||
return {
|
||||
content: [{ type: "text", text: content }],
|
||||
};
|
||||
}
|
||||
|
||||
case "read_media_file": {
|
||||
const parsed = ReadMediaFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const extension = path.extname(validPath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".flac": "audio/flac",
|
||||
};
|
||||
const mimeType = mimeTypes[extension] || "application/octet-stream";
|
||||
const data = await readFileAsBase64Stream(validPath);
|
||||
const type = mimeType.startsWith("image/")
|
||||
? "image"
|
||||
: mimeType.startsWith("audio/")
|
||||
? "audio"
|
||||
: "blob";
|
||||
return {
|
||||
content: [{ type, data, mimeType }],
|
||||
};
|
||||
}
|
||||
|
||||
case "read_multiple_files": {
|
||||
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`);
|
||||
}
|
||||
const results = await Promise.all(
|
||||
parsed.data.paths.map(async (filePath: string) => {
|
||||
try {
|
||||
const validPath = await validatePath(filePath);
|
||||
const content = await readFileContent(validPath);
|
||||
return `${filePath}:\n${content}\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return `${filePath}: Error - ${errorMessage}`;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: results.join("\n---\n") }],
|
||||
};
|
||||
}
|
||||
|
||||
case "write_file": {
|
||||
const parsed = WriteFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
await writeFileContent(validPath, parsed.data.content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
|
||||
};
|
||||
}
|
||||
|
||||
case "edit_file": {
|
||||
const parsed = EditFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
|
||||
return {
|
||||
content: [{ type: "text", text: result }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_directory": {
|
||||
const parsed = CreateDirectoryArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
await fs.mkdir(validPath, { recursive: true });
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_directory": {
|
||||
const parsed = ListDirectoryArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
const formatted = entries
|
||||
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
||||
.join("\n");
|
||||
return {
|
||||
content: [{ type: "text", text: formatted }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_directory_with_sizes": {
|
||||
const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
|
||||
// Get detailed information for each entry
|
||||
const detailedEntries = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(validPath, entry.name);
|
||||
try {
|
||||
const stats = await fs.stat(entryPath);
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: 0,
|
||||
mtime: new Date(0)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Sort entries based on sortBy parameter
|
||||
const sortedEntries = [...detailedEntries].sort((a, b) => {
|
||||
if (parsed.data.sortBy === 'size') {
|
||||
return b.size - a.size; // Descending by size
|
||||
}
|
||||
// Default sort by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Format the output
|
||||
const formattedEntries = sortedEntries.map(entry =>
|
||||
`${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${
|
||||
entry.isDirectory ? "" : formatSize(entry.size).padStart(10)
|
||||
}`
|
||||
);
|
||||
|
||||
// Add summary
|
||||
const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
|
||||
const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
|
||||
const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
|
||||
|
||||
const summary = [
|
||||
"",
|
||||
`Total: ${totalFiles} files, ${totalDirs} directories`,
|
||||
`Combined size: ${formatSize(totalSize)}`
|
||||
];
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: [...formattedEntries, ...summary].join("\n")
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case "directory_tree": {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||
}
|
||||
|
||||
interface TreeEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
children?: TreeEntry[];
|
||||
}
|
||||
const rootPath = parsed.data.path;
|
||||
|
||||
async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise<TreeEntry[]> {
|
||||
const validPath = await validatePath(currentPath);
|
||||
const entries = await fs.readdir(validPath, {withFileTypes: true});
|
||||
const result: TreeEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
|
||||
const shouldExclude = excludePatterns.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
return minimatch(relativePath, pattern, {dot: true});
|
||||
}
|
||||
// For files: match exact name or as part of path
|
||||
// For directories: match as directory path
|
||||
return minimatch(relativePath, pattern, {dot: true}) ||
|
||||
minimatch(relativePath, `**/${pattern}`, {dot: true}) ||
|
||||
minimatch(relativePath, `**/${pattern}/**`, {dot: true});
|
||||
});
|
||||
if (shouldExclude)
|
||||
continue;
|
||||
|
||||
const entryData: TreeEntry = {
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file'
|
||||
};
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subPath = path.join(currentPath, entry.name);
|
||||
entryData.children = await buildTree(subPath, excludePatterns);
|
||||
}
|
||||
|
||||
result.push(entryData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const treeData = await buildTree(rootPath, parsed.data.excludePatterns);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(treeData, null, 2)
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case "move_file": {
|
||||
const parsed = MoveFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
|
||||
}
|
||||
const validSourcePath = await validatePath(parsed.data.source);
|
||||
const validDestPath = await validatePath(parsed.data.destination);
|
||||
await fs.rename(validSourcePath, validDestPath);
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_files": {
|
||||
const parsed = SearchFilesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const results = await searchFilesWithValidation(validPath, parsed.data.pattern, allowedDirectories, { excludePatterns: parsed.data.excludePatterns });
|
||||
return {
|
||||
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_file_info": {
|
||||
const parsed = GetFileInfoArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const info = await getFileStats(validPath);
|
||||
return {
|
||||
content: [{ type: "text", text: Object.entries(info)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n") }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_allowed_directories": {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
server.registerTool({
|
||||
name: "read_text_file",
|
||||
description:
|
||||
"Read the complete contents of a file from the file system as text. " +
|
||||
"Handles various text encodings and provides detailed error messages " +
|
||||
"if the file cannot be read. Use this tool when you need to examine " +
|
||||
"the contents of a single file. Use the 'head' parameter to read only " +
|
||||
"the first N lines of a file, or the 'tail' parameter to read only " +
|
||||
"the last N lines of a file. Operates on the file as text regardless of extension. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ReadTextFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
|
||||
if (parsed.data.head && parsed.data.tail) {
|
||||
throw new Error("Cannot specify both head and tail parameters simultaneously");
|
||||
}
|
||||
|
||||
if (parsed.data.tail) {
|
||||
// Use memory-efficient tail implementation for large files
|
||||
const tailContent = await tailFile(validPath, parsed.data.tail);
|
||||
return {
|
||||
content: [{ type: "text", text: tailContent }],
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.data.head) {
|
||||
// Use memory-efficient head implementation for large files
|
||||
const headContent = await headFile(validPath, parsed.data.head);
|
||||
return {
|
||||
content: [{ type: "text", text: headContent }],
|
||||
};
|
||||
}
|
||||
const content = await readFileContent(validPath);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
||||
isError: true,
|
||||
content: [{ type: "text", text: content }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "read_media_file",
|
||||
description:
|
||||
"Read an image or audio file. Returns the base64 encoded data and MIME type. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadMediaFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ReadMediaFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const extension = path.extname(validPath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".flac": "audio/flac",
|
||||
};
|
||||
const mimeType = mimeTypes[extension] || "application/octet-stream";
|
||||
const data = await readFileAsBase64Stream(validPath);
|
||||
const type = mimeType.startsWith("image/")
|
||||
? "image"
|
||||
: mimeType.startsWith("audio/")
|
||||
? "audio"
|
||||
: "blob";
|
||||
return {
|
||||
content: [{ type, data, mimeType }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "read_multiple_files",
|
||||
description:
|
||||
"Read the contents of multiple files simultaneously. This is more " +
|
||||
"efficient than reading files one by one when you need to analyze " +
|
||||
"or compare multiple files. Each file's content is returned with its " +
|
||||
"path as a reference. Failed reads for individual files won't stop " +
|
||||
"the entire operation. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`);
|
||||
}
|
||||
const results = await Promise.all(
|
||||
parsed.data.paths.map(async (filePath: string) => {
|
||||
try {
|
||||
const validPath = await validatePath(filePath);
|
||||
const content = await readFileContent(validPath);
|
||||
return `${filePath}:\n${content}\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return `${filePath}: Error - ${errorMessage}`;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: results.join("\n---\n") }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "write_file",
|
||||
description:
|
||||
"Create a new file or completely overwrite an existing file with new content. " +
|
||||
"Use with caution as it will overwrite existing files without warning. " +
|
||||
"Handles text content with proper encoding. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = WriteFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
await writeFileContent(validPath, parsed.data.content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "edit_file",
|
||||
description:
|
||||
"Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
||||
"with new content. Returns a git-style diff showing the changes made. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = EditFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
|
||||
return {
|
||||
content: [{ type: "text", text: result }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "create_directory",
|
||||
description:
|
||||
"Create a new directory or ensure a directory exists. Can create multiple " +
|
||||
"nested directories in one operation. If the directory already exists, " +
|
||||
"this operation will succeed silently. Perfect for setting up directory " +
|
||||
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = CreateDirectoryArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
await fs.mkdir(validPath, { recursive: true });
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "list_directory",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is essential for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ListDirectoryArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
const formatted = entries
|
||||
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
||||
.join("\n");
|
||||
return {
|
||||
content: [{ type: "text", text: formatted }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "list_directory_with_sizes",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path, including sizes. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is useful for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryWithSizesArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
|
||||
// Get detailed information for each entry
|
||||
const detailedEntries = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(validPath, entry.name);
|
||||
try {
|
||||
const stats = await fs.stat(entryPath);
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: 0,
|
||||
mtime: new Date(0)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Sort entries based on sortBy parameter
|
||||
const sortedEntries = [...detailedEntries].sort((a, b) => {
|
||||
if (parsed.data.sortBy === 'size') {
|
||||
return b.size - a.size; // Descending by size
|
||||
}
|
||||
// Default sort by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Format the output
|
||||
const formattedEntries = sortedEntries.map(entry =>
|
||||
`${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${
|
||||
entry.isDirectory ? "" : formatSize(entry.size).padStart(10)
|
||||
}`
|
||||
);
|
||||
|
||||
// Add summary
|
||||
const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
|
||||
const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
|
||||
const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
|
||||
|
||||
const summary = [
|
||||
"",
|
||||
`Total: ${totalFiles} files, ${totalDirs} directories`,
|
||||
`Combined size: ${formatSize(totalSize)}`
|
||||
];
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: [...formattedEntries, ...summary].join("\n")
|
||||
}],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "directory_tree",
|
||||
description:
|
||||
"Get a recursive tree view of files and directories as a JSON structure. " +
|
||||
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||
"Files have no children array, while directories always have a children array (which may be empty). " +
|
||||
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||
}
|
||||
|
||||
interface TreeEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
children?: TreeEntry[];
|
||||
}
|
||||
const rootPath = parsed.data.path;
|
||||
|
||||
async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise<TreeEntry[]> {
|
||||
const validPath = await validatePath(currentPath);
|
||||
const entries = await fs.readdir(validPath, {withFileTypes: true});
|
||||
const result: TreeEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
|
||||
const shouldExclude = excludePatterns.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
return minimatch(relativePath, pattern, {dot: true});
|
||||
}
|
||||
// For files: match exact name or as part of path
|
||||
// For directories: match as directory path
|
||||
return minimatch(relativePath, pattern, {dot: true}) ||
|
||||
minimatch(relativePath, `**/${pattern}`, {dot: true}) ||
|
||||
minimatch(relativePath, `**/${pattern}/**`, {dot: true});
|
||||
});
|
||||
if (shouldExclude)
|
||||
continue;
|
||||
|
||||
const entryData: TreeEntry = {
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file'
|
||||
};
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subPath = path.join(currentPath, entry.name);
|
||||
entryData.children = await buildTree(subPath, excludePatterns);
|
||||
}
|
||||
|
||||
result.push(entryData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const treeData = await buildTree(rootPath, parsed.data.excludePatterns);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(treeData, null, 2)
|
||||
}],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "move_file",
|
||||
description:
|
||||
"Move or rename files and directories. Can move files between directories " +
|
||||
"and rename them in a single operation. If the destination exists, the " +
|
||||
"operation will fail. Works across different directories and can be used " +
|
||||
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = MoveFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
|
||||
}
|
||||
const validSourcePath = await validatePath(parsed.data.source);
|
||||
const validDestPath = await validatePath(parsed.data.destination);
|
||||
await fs.rename(validSourcePath, validDestPath);
|
||||
return {
|
||||
content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "search_files",
|
||||
description:
|
||||
"Recursively search for files and directories matching a pattern. " +
|
||||
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
|
||||
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
|
||||
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
|
||||
"Only searches within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = SearchFilesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const results = await searchFilesWithValidation(validPath, parsed.data.pattern, allowedDirectories, { excludePatterns: parsed.data.excludePatterns });
|
||||
return {
|
||||
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "get_file_info",
|
||||
description:
|
||||
"Retrieve detailed metadata about a file or directory. Returns comprehensive " +
|
||||
"information including size, creation time, last modified time, permissions, " +
|
||||
"and type. This tool is perfect for understanding file characteristics " +
|
||||
"without reading the actual content. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput,
|
||||
handler: async (args: any) => {
|
||||
const parsed = GetFileInfoArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const info = await getFileStats(validPath);
|
||||
return {
|
||||
content: [{ type: "text", text: Object.entries(info)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n") }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "list_allowed_directories",
|
||||
description:
|
||||
"Returns the list of directories that this server is allowed to access. " +
|
||||
"Subdirectories within these allowed directories are also accessible. " +
|
||||
"Use this to understand which directories and their nested paths are available " +
|
||||
"before trying to access files.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
||||
}],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"test": "jest --config=jest.config.cjs --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"diff": "^5.1.0",
|
||||
"glob": "^10.3.10",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -206,230 +202,226 @@ const server = new Server({
|
||||
},
|
||||
},);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "create_entities",
|
||||
description: "Create multiple new entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entities: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "The name of the entity" },
|
||||
entityType: { type: "string", description: "The type of the entity" },
|
||||
observations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of observation contents associated with the entity"
|
||||
},
|
||||
},
|
||||
required: ["name", "entityType", "observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["entities"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_relations",
|
||||
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
relations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string", description: "The name of the entity where the relation starts" },
|
||||
to: { type: "string", description: "The name of the entity where the relation ends" },
|
||||
relationType: { type: "string", description: "The type of the relation" },
|
||||
},
|
||||
required: ["from", "to", "relationType"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["relations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add_observations",
|
||||
description: "Add new observations to existing entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
server.registerTool({
|
||||
name: "create_entities",
|
||||
description: "Create multiple new entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entities: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "The name of the entity" },
|
||||
entityType: { type: "string", description: "The type of the entity" },
|
||||
observations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityName: { type: "string", description: "The name of the entity to add the observations to" },
|
||||
contents: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of observation contents to add"
|
||||
},
|
||||
},
|
||||
required: ["entityName", "contents"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete_entities",
|
||||
description: "Delete multiple entities and their associated relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityNames: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of entity names to delete"
|
||||
description: "An array of observation contents associated with the entity"
|
||||
},
|
||||
},
|
||||
required: ["entityNames"],
|
||||
required: ["name", "entityType", "observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete_observations",
|
||||
description: "Delete specific observations from entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
deletions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityName: { type: "string", description: "The name of the entity containing the observations" },
|
||||
observations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of observations to delete"
|
||||
},
|
||||
},
|
||||
required: ["entityName", "observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["deletions"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete_relations",
|
||||
description: "Delete multiple relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
relations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string", description: "The name of the entity where the relation starts" },
|
||||
to: { type: "string", description: "The name of the entity where the relation ends" },
|
||||
relationType: { type: "string", description: "The type of the relation" },
|
||||
},
|
||||
required: ["from", "to", "relationType"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
description: "An array of relations to delete"
|
||||
},
|
||||
},
|
||||
required: ["relations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_graph",
|
||||
description: "Read the entire knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search_nodes",
|
||||
description: "Search for nodes in the knowledge graph based on a query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
|
||||
},
|
||||
required: ["query"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "open_nodes",
|
||||
description: "Open specific nodes in the knowledge graph by their names",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of entity names to retrieve",
|
||||
},
|
||||
},
|
||||
required: ["names"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
required: ["entities"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
server.registerTool({
|
||||
name: "create_relations",
|
||||
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
relations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string", description: "The name of the entity where the relation starts" },
|
||||
to: { type: "string", description: "The name of the entity where the relation ends" },
|
||||
relationType: { type: "string", description: "The type of the relation" },
|
||||
},
|
||||
required: ["from", "to", "relationType"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["relations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
if (name === "read_graph") {
|
||||
server.registerTool({
|
||||
name: "add_observations",
|
||||
description: "Add new observations to existing entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
observations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityName: { type: "string", description: "The name of the entity to add the observations to" },
|
||||
contents: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of observation contents to add"
|
||||
},
|
||||
},
|
||||
required: ["entityName", "contents"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "delete_entities",
|
||||
description: "Delete multiple entities and their associated relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityNames: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of entity names to delete"
|
||||
},
|
||||
},
|
||||
required: ["entityNames"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
await knowledgeGraphManager.deleteEntities(args.entityNames as string[]);
|
||||
return { content: [{ type: "text", text: "Entities deleted successfully" }] };
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "delete_observations",
|
||||
description: "Delete specific observations from entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
deletions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityName: { type: "string", description: "The name of the entity containing the observations" },
|
||||
observations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of observations to delete"
|
||||
},
|
||||
},
|
||||
required: ["entityName", "observations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["deletions"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]);
|
||||
return { content: [{ type: "text", text: "Observations deleted successfully" }] };
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "delete_relations",
|
||||
description: "Delete multiple relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
relations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string", description: "The name of the entity where the relation starts" },
|
||||
to: { type: "string", description: "The name of the entity where the relation ends" },
|
||||
relationType: { type: "string", description: "The type of the relation" },
|
||||
},
|
||||
required: ["from", "to", "relationType"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
description: "An array of relations to delete"
|
||||
},
|
||||
},
|
||||
required: ["relations"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
await knowledgeGraphManager.deleteRelations(args.relations as Relation[]);
|
||||
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
||||
}
|
||||
});
|
||||
|
||||
server.registerTool({
|
||||
name: "read_graph",
|
||||
description: "Read the entire knowledge graph",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async () => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
if (!args) {
|
||||
throw new Error(`No arguments provided for tool: ${name}`);
|
||||
server.registerTool({
|
||||
name: "search_nodes",
|
||||
description: "Search for nodes in the knowledge graph based on a query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
|
||||
},
|
||||
required: ["query"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
switch (name) {
|
||||
case "create_entities":
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] };
|
||||
case "create_relations":
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] };
|
||||
case "add_observations":
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] };
|
||||
case "delete_entities":
|
||||
await knowledgeGraphManager.deleteEntities(args.entityNames as string[]);
|
||||
return { content: [{ type: "text", text: "Entities deleted successfully" }] };
|
||||
case "delete_observations":
|
||||
await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]);
|
||||
return { content: [{ type: "text", text: "Observations deleted successfully" }] };
|
||||
case "delete_relations":
|
||||
await knowledgeGraphManager.deleteRelations(args.relations as Relation[]);
|
||||
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
||||
case "search_nodes":
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string), null, 2) }] };
|
||||
case "open_nodes":
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[]), null, 2) }] };
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
server.registerTool({
|
||||
name: "open_nodes",
|
||||
description: "Open specific nodes in the knowledge graph by their names",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "An array of entity names to retrieve",
|
||||
},
|
||||
},
|
||||
required: ["names"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: async (args) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[]), null, 2) }] };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1"
|
||||
"@modelcontextprotocol/sdk": "^1.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
// Fixed chalk import for ESM
|
||||
@@ -255,22 +253,13 @@ const server = new Server(
|
||||
|
||||
const thinkingServer = new SequentialThinkingServer();
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [SEQUENTIAL_THINKING_TOOL],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "sequentialthinking") {
|
||||
return thinkingServer.processThought(request.params.arguments);
|
||||
server.registerTool({
|
||||
name: SEQUENTIAL_THINKING_TOOL.name,
|
||||
description: SEQUENTIAL_THINKING_TOOL.description,
|
||||
inputSchema: SEQUENTIAL_THINKING_TOOL.inputSchema,
|
||||
handler: async (args) => {
|
||||
return thinkingServer.processThought(args);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Unknown tool: ${request.params.name}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"chalk": "^5.3.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user