mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-02-19 11:54:58 -05:00
Merge branch 'main' into convert-everything-to-modern-api
This commit is contained in:
13
.github/workflows/claude.yml
vendored
13
.github/workflows/claude.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
@@ -42,10 +42,7 @@ jobs:
|
||||
|
||||
# Trigger when assigned to an issue
|
||||
assignee_trigger: "claude"
|
||||
|
||||
# Allow Claude to run bash
|
||||
# This should be safe given the repo is already public
|
||||
allowed_tools: "Bash"
|
||||
|
||||
custom_instructions: |
|
||||
If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block."
|
||||
|
||||
14
.github/workflows/typescript.yml
vendored
14
.github/workflows/typescript.yml
vendored
@@ -41,21 +41,9 @@ jobs:
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm ci
|
||||
|
||||
- name: Check if tests exist
|
||||
id: check-tests
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: |
|
||||
if npm run test --silent 2>/dev/null; then
|
||||
echo "has-tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has-tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check-tests.outputs.has-tests == 'true'
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm test
|
||||
run: npm test --if-present
|
||||
|
||||
build:
|
||||
needs: [detect-packages, test]
|
||||
|
||||
@@ -854,6 +854,7 @@ A growing set of community-developed and maintained servers demonstrates various
|
||||
- **[GraphQL](https://github.com/drestrepom/mcp_graphql)** - Comprehensive GraphQL API integration that automatically exposes each GraphQL query as a separate tool.
|
||||
- **[GraphQL Schema](https://github.com/hannesj/mcp-graphql-schema)** - Allow LLMs to explore large GraphQL schemas without bloating the context.
|
||||
- **[Graylog](https://github.com/Pranavj17/mcp-server-graylog)** - Search Graylog logs by absolute/relative timestamps, filter by streams, and debug production issues directly from Claude Desktop.
|
||||
- **[Grok-MCP](https://github.com/merterbak/Grok-MCP)** - MCP server for xAI’s API featuring the latest Grok models, image analysis & generation, and web search.
|
||||
- **[gx-mcp-server](https://github.com/davidf9999/gx-mcp-server)** - Expose Great Expectations data validation and quality checks as MCP tools for AI agents.
|
||||
- **[HackMD](https://github.com/yuna0x0/hackmd-mcp)** (by yuna0x0) - An MCP server for HackMD, a collaborative markdown editor. It allows users to create, read, and update documents in HackMD using the Model Context Protocol.
|
||||
- **[HAProxy](https://github.com/tuannvm/haproxy-mcp-server)** - A Model Context Protocol (MCP) server for HAProxy implemented in Go, leveraging HAProxy Runtime API.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
"exclude": [
|
||||
"**/__tests__/**",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
"**/*.spec.ts",
|
||||
"vitest.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -226,243 +223,235 @@ export class KnowledgeGraphManager {
|
||||
|
||||
let knowledgeGraphManager: KnowledgeGraphManager;
|
||||
|
||||
// Zod schemas for entities and relations
|
||||
const EntitySchema = z.object({
|
||||
name: z.string().describe("The name of the entity"),
|
||||
entityType: z.string().describe("The type of the entity"),
|
||||
observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
|
||||
});
|
||||
|
||||
const RelationSchema = z.object({
|
||||
from: z.string().describe("The name of the entity where the relation starts"),
|
||||
to: z.string().describe("The name of the entity where the relation ends"),
|
||||
relationType: z.string().describe("The type of the relation")
|
||||
});
|
||||
|
||||
// The server instance and tools exposed to Claude
|
||||
const server = new Server({
|
||||
const server = new McpServer({
|
||||
name: "memory-server",
|
||||
version: "0.6.3",
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
});
|
||||
|
||||
// Register create_entities tool
|
||||
server.registerTool(
|
||||
"create_entities",
|
||||
{
|
||||
title: "Create Entities",
|
||||
description: "Create multiple new entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
entities: z.array(EntitySchema)
|
||||
},
|
||||
},);
|
||||
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (name === "read_graph") {
|
||||
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] };
|
||||
outputSchema: {
|
||||
entities: z.array(EntitySchema)
|
||||
}
|
||||
},
|
||||
async ({ entities }) => {
|
||||
const result = await knowledgeGraphManager.createEntities(entities);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
||||
structuredContent: { entities: result }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
if (!args) {
|
||||
throw new Error(`No arguments provided for tool: ${name}`);
|
||||
// Register create_relations tool
|
||||
server.registerTool(
|
||||
"create_relations",
|
||||
{
|
||||
title: "Create Relations",
|
||||
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
|
||||
inputSchema: {
|
||||
relations: z.array(RelationSchema)
|
||||
},
|
||||
outputSchema: {
|
||||
relations: z.array(RelationSchema)
|
||||
}
|
||||
},
|
||||
async ({ relations }) => {
|
||||
const result = await knowledgeGraphManager.createRelations(relations);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
||||
structuredContent: { relations: result }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
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}`);
|
||||
// Register add_observations tool
|
||||
server.registerTool(
|
||||
"add_observations",
|
||||
{
|
||||
title: "Add Observations",
|
||||
description: "Add new observations to existing entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
observations: z.array(z.object({
|
||||
entityName: z.string().describe("The name of the entity to add the observations to"),
|
||||
contents: z.array(z.string()).describe("An array of observation contents to add")
|
||||
}))
|
||||
},
|
||||
outputSchema: {
|
||||
results: z.array(z.object({
|
||||
entityName: z.string(),
|
||||
addedObservations: z.array(z.string())
|
||||
}))
|
||||
}
|
||||
},
|
||||
async ({ observations }) => {
|
||||
const result = await knowledgeGraphManager.addObservations(observations);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
||||
structuredContent: { results: result }
|
||||
};
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Register delete_entities tool
|
||||
server.registerTool(
|
||||
"delete_entities",
|
||||
{
|
||||
title: "Delete Entities",
|
||||
description: "Delete multiple entities and their associated relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
entityNames: z.array(z.string()).describe("An array of entity names to delete")
|
||||
},
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
message: z.string()
|
||||
}
|
||||
},
|
||||
async ({ entityNames }) => {
|
||||
await knowledgeGraphManager.deleteEntities(entityNames);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "Entities deleted successfully" }],
|
||||
structuredContent: { success: true, message: "Entities deleted successfully" }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register delete_observations tool
|
||||
server.registerTool(
|
||||
"delete_observations",
|
||||
{
|
||||
title: "Delete Observations",
|
||||
description: "Delete specific observations from entities in the knowledge graph",
|
||||
inputSchema: {
|
||||
deletions: z.array(z.object({
|
||||
entityName: z.string().describe("The name of the entity containing the observations"),
|
||||
observations: z.array(z.string()).describe("An array of observations to delete")
|
||||
}))
|
||||
},
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
message: z.string()
|
||||
}
|
||||
},
|
||||
async ({ deletions }) => {
|
||||
await knowledgeGraphManager.deleteObservations(deletions);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "Observations deleted successfully" }],
|
||||
structuredContent: { success: true, message: "Observations deleted successfully" }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register delete_relations tool
|
||||
server.registerTool(
|
||||
"delete_relations",
|
||||
{
|
||||
title: "Delete Relations",
|
||||
description: "Delete multiple relations from the knowledge graph",
|
||||
inputSchema: {
|
||||
relations: z.array(RelationSchema).describe("An array of relations to delete")
|
||||
},
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
message: z.string()
|
||||
}
|
||||
},
|
||||
async ({ relations }) => {
|
||||
await knowledgeGraphManager.deleteRelations(relations);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "Relations deleted successfully" }],
|
||||
structuredContent: { success: true, message: "Relations deleted successfully" }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register read_graph tool
|
||||
server.registerTool(
|
||||
"read_graph",
|
||||
{
|
||||
title: "Read Graph",
|
||||
description: "Read the entire knowledge graph",
|
||||
inputSchema: {},
|
||||
outputSchema: {
|
||||
entities: z.array(EntitySchema),
|
||||
relations: z.array(RelationSchema)
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
const graph = await knowledgeGraphManager.readGraph();
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
|
||||
structuredContent: { ...graph }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register search_nodes tool
|
||||
server.registerTool(
|
||||
"search_nodes",
|
||||
{
|
||||
title: "Search Nodes",
|
||||
description: "Search for nodes in the knowledge graph based on a query",
|
||||
inputSchema: {
|
||||
query: z.string().describe("The search query to match against entity names, types, and observation content")
|
||||
},
|
||||
outputSchema: {
|
||||
entities: z.array(EntitySchema),
|
||||
relations: z.array(RelationSchema)
|
||||
}
|
||||
},
|
||||
async ({ query }) => {
|
||||
const graph = await knowledgeGraphManager.searchNodes(query);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
|
||||
structuredContent: { ...graph }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register open_nodes tool
|
||||
server.registerTool(
|
||||
"open_nodes",
|
||||
{
|
||||
title: "Open Nodes",
|
||||
description: "Open specific nodes in the knowledge graph by their names",
|
||||
inputSchema: {
|
||||
names: z.array(z.string()).describe("An array of entity names to retrieve")
|
||||
},
|
||||
outputSchema: {
|
||||
entities: z.array(EntitySchema),
|
||||
relations: z.array(RelationSchema)
|
||||
}
|
||||
},
|
||||
async ({ names }) => {
|
||||
const graph = await knowledgeGraphManager.openNodes(names);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
|
||||
structuredContent: { ...graph }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
async function main() {
|
||||
// Initialize memory file path with backward compatibility
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"vitest.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,107 +22,8 @@ describe('SequentialThinkingServer', () => {
|
||||
server = new SequentialThinkingServer();
|
||||
});
|
||||
|
||||
describe('processThought - validation', () => {
|
||||
it('should reject input with missing thought', () => {
|
||||
const input = {
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 3,
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid thought');
|
||||
});
|
||||
|
||||
it('should reject input with non-string thought', () => {
|
||||
const input = {
|
||||
thought: 123,
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 3,
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid thought');
|
||||
});
|
||||
|
||||
it('should reject input with missing thoughtNumber', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
totalThoughts: 3,
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid thoughtNumber');
|
||||
});
|
||||
|
||||
it('should reject input with non-number thoughtNumber', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
thoughtNumber: '1',
|
||||
totalThoughts: 3,
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid thoughtNumber');
|
||||
});
|
||||
|
||||
it('should reject input with missing totalThoughts', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
thoughtNumber: 1,
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid totalThoughts');
|
||||
});
|
||||
|
||||
it('should reject input with non-number totalThoughts', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: '3',
|
||||
nextThoughtNeeded: true
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid totalThoughts');
|
||||
});
|
||||
|
||||
it('should reject input with missing nextThoughtNeeded', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 3
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid nextThoughtNeeded');
|
||||
});
|
||||
|
||||
it('should reject input with non-boolean nextThoughtNeeded', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 3,
|
||||
nextThoughtNeeded: 'true'
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid nextThoughtNeeded');
|
||||
});
|
||||
});
|
||||
// Note: Input validation tests removed - validation now happens at the tool
|
||||
// registration layer via Zod schemas before processThought is called
|
||||
|
||||
describe('processThought - valid inputs', () => {
|
||||
it('should accept valid basic thought', () => {
|
||||
@@ -275,19 +176,6 @@ describe('SequentialThinkingServer', () => {
|
||||
});
|
||||
|
||||
describe('processThought - edge cases', () => {
|
||||
it('should reject empty thought string', () => {
|
||||
const input = {
|
||||
thought: '',
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 1,
|
||||
nextThoughtNeeded: false
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid thought');
|
||||
});
|
||||
|
||||
it('should handle very long thought strings', () => {
|
||||
const input = {
|
||||
thought: 'a'.repeat(10000),
|
||||
@@ -349,25 +237,6 @@ describe('SequentialThinkingServer', () => {
|
||||
expect(result.content[0]).toHaveProperty('text');
|
||||
});
|
||||
|
||||
it('should return correct error structure on failure', () => {
|
||||
const input = {
|
||||
thought: 'Test',
|
||||
thoughtNumber: 1,
|
||||
totalThoughts: 1
|
||||
// missing nextThoughtNeeded
|
||||
};
|
||||
|
||||
const result = server.processThought(input);
|
||||
|
||||
expect(result).toHaveProperty('isError', true);
|
||||
expect(result).toHaveProperty('content');
|
||||
expect(Array.isArray(result.content)).toBe(true);
|
||||
|
||||
const errorData = JSON.parse(result.content[0].text);
|
||||
expect(errorData).toHaveProperty('error');
|
||||
expect(errorData).toHaveProperty('status', 'failed');
|
||||
});
|
||||
|
||||
it('should return valid JSON in response', () => {
|
||||
const input = {
|
||||
thought: 'Test thought',
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import { SequentialThinkingServer } from './lib.js';
|
||||
|
||||
const SEQUENTIAL_THINKING_TOOL: Tool = {
|
||||
name: "sequentialthinking",
|
||||
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
|
||||
const server = new McpServer({
|
||||
name: "sequential-thinking-server",
|
||||
version: "0.2.0",
|
||||
});
|
||||
|
||||
const thinkingServer = new SequentialThinkingServer();
|
||||
|
||||
server.registerTool(
|
||||
"sequentialthinking",
|
||||
{
|
||||
title: "Sequential Thinking",
|
||||
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
|
||||
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
|
||||
Each thought can build on, question, or revise previous insights as understanding deepens.
|
||||
|
||||
@@ -37,13 +42,13 @@ Key features:
|
||||
|
||||
Parameters explained:
|
||||
- thought: Your current thinking step, which can include:
|
||||
* Regular analytical steps
|
||||
* Revisions of previous thoughts
|
||||
* Questions about previous decisions
|
||||
* Realizations about needing more analysis
|
||||
* Changes in approach
|
||||
* Hypothesis generation
|
||||
* Hypothesis verification
|
||||
* Regular analytical steps
|
||||
* Revisions of previous thoughts
|
||||
* Questions about previous decisions
|
||||
* Realizations about needing more analysis
|
||||
* Changes in approach
|
||||
* Hypothesis generation
|
||||
* Hypothesis verification
|
||||
- nextThoughtNeeded: True if you need more thinking, even if at what seemed like the end
|
||||
- thoughtNumber: Current number in sequence (can go beyond initial total if needed)
|
||||
- totalThoughts: Current estimate of thoughts needed (can be adjusted up/down)
|
||||
@@ -65,86 +70,42 @@ You should:
|
||||
9. Repeat the process until satisfied with the solution
|
||||
10. Provide a single, ideally correct answer as the final output
|
||||
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
thought: {
|
||||
type: "string",
|
||||
description: "Your current thinking step"
|
||||
},
|
||||
nextThoughtNeeded: {
|
||||
type: "boolean",
|
||||
description: "Whether another thought step is needed"
|
||||
},
|
||||
thoughtNumber: {
|
||||
type: "integer",
|
||||
description: "Current thought number (numeric value, e.g., 1, 2, 3)",
|
||||
minimum: 1
|
||||
},
|
||||
totalThoughts: {
|
||||
type: "integer",
|
||||
description: "Estimated total thoughts needed (numeric value, e.g., 5, 10)",
|
||||
minimum: 1
|
||||
},
|
||||
isRevision: {
|
||||
type: "boolean",
|
||||
description: "Whether this revises previous thinking"
|
||||
},
|
||||
revisesThought: {
|
||||
type: "integer",
|
||||
description: "Which thought is being reconsidered",
|
||||
minimum: 1
|
||||
},
|
||||
branchFromThought: {
|
||||
type: "integer",
|
||||
description: "Branching point thought number",
|
||||
minimum: 1
|
||||
},
|
||||
branchId: {
|
||||
type: "string",
|
||||
description: "Branch identifier"
|
||||
},
|
||||
needsMoreThoughts: {
|
||||
type: "boolean",
|
||||
description: "If more thoughts are needed"
|
||||
}
|
||||
inputSchema: {
|
||||
thought: z.string().describe("Your current thinking step"),
|
||||
nextThoughtNeeded: z.boolean().describe("Whether another thought step is needed"),
|
||||
thoughtNumber: z.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"),
|
||||
totalThoughts: z.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"),
|
||||
isRevision: z.boolean().optional().describe("Whether this revises previous thinking"),
|
||||
revisesThought: z.number().int().min(1).optional().describe("Which thought is being reconsidered"),
|
||||
branchFromThought: z.number().int().min(1).optional().describe("Branching point thought number"),
|
||||
branchId: z.string().optional().describe("Branch identifier"),
|
||||
needsMoreThoughts: z.boolean().optional().describe("If more thoughts are needed")
|
||||
},
|
||||
outputSchema: {
|
||||
thoughtNumber: z.number(),
|
||||
totalThoughts: z.number(),
|
||||
nextThoughtNeeded: z.boolean(),
|
||||
branches: z.array(z.string()),
|
||||
thoughtHistoryLength: z.number()
|
||||
},
|
||||
required: ["thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"]
|
||||
}
|
||||
};
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "sequential-thinking-server",
|
||||
version: "0.2.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
async (args) => {
|
||||
const result = thinkingServer.processThought(args);
|
||||
|
||||
if (result.isError) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse the JSON response to get structured content
|
||||
const parsedContent = JSON.parse(result.content[0].text);
|
||||
|
||||
return {
|
||||
content: result.content,
|
||||
structuredContent: parsedContent
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Unknown tool: ${request.params.name}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
@@ -21,35 +21,6 @@ export class SequentialThinkingServer {
|
||||
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
||||
}
|
||||
|
||||
private validateThoughtData(input: unknown): ThoughtData {
|
||||
const data = input as Record<string, unknown>;
|
||||
|
||||
if (!data.thought || typeof data.thought !== 'string') {
|
||||
throw new Error('Invalid thought: must be a string');
|
||||
}
|
||||
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') {
|
||||
throw new Error('Invalid thoughtNumber: must be a number');
|
||||
}
|
||||
if (!data.totalThoughts || typeof data.totalThoughts !== 'number') {
|
||||
throw new Error('Invalid totalThoughts: must be a number');
|
||||
}
|
||||
if (typeof data.nextThoughtNeeded !== 'boolean') {
|
||||
throw new Error('Invalid nextThoughtNeeded: must be a boolean');
|
||||
}
|
||||
|
||||
return {
|
||||
thought: data.thought,
|
||||
thoughtNumber: data.thoughtNumber,
|
||||
totalThoughts: data.totalThoughts,
|
||||
nextThoughtNeeded: data.nextThoughtNeeded,
|
||||
isRevision: data.isRevision as boolean | undefined,
|
||||
revisesThought: data.revisesThought as number | undefined,
|
||||
branchFromThought: data.branchFromThought as number | undefined,
|
||||
branchId: data.branchId as string | undefined,
|
||||
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private formatThought(thoughtData: ThoughtData): string {
|
||||
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } = thoughtData;
|
||||
|
||||
@@ -78,35 +49,35 @@ export class SequentialThinkingServer {
|
||||
└${border}┘`;
|
||||
}
|
||||
|
||||
public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } {
|
||||
public processThought(input: ThoughtData): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
|
||||
try {
|
||||
const validatedInput = this.validateThoughtData(input);
|
||||
|
||||
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
|
||||
validatedInput.totalThoughts = validatedInput.thoughtNumber;
|
||||
// Validation happens at the tool registration layer via Zod
|
||||
// Adjust totalThoughts if thoughtNumber exceeds it
|
||||
if (input.thoughtNumber > input.totalThoughts) {
|
||||
input.totalThoughts = input.thoughtNumber;
|
||||
}
|
||||
|
||||
this.thoughtHistory.push(validatedInput);
|
||||
this.thoughtHistory.push(input);
|
||||
|
||||
if (validatedInput.branchFromThought && validatedInput.branchId) {
|
||||
if (!this.branches[validatedInput.branchId]) {
|
||||
this.branches[validatedInput.branchId] = [];
|
||||
if (input.branchFromThought && input.branchId) {
|
||||
if (!this.branches[input.branchId]) {
|
||||
this.branches[input.branchId] = [];
|
||||
}
|
||||
this.branches[validatedInput.branchId].push(validatedInput);
|
||||
this.branches[input.branchId].push(input);
|
||||
}
|
||||
|
||||
if (!this.disableThoughtLogging) {
|
||||
const formattedThought = this.formatThought(validatedInput);
|
||||
const formattedThought = this.formatThought(input);
|
||||
console.error(formattedThought);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
thoughtNumber: validatedInput.thoughtNumber,
|
||||
totalThoughts: validatedInput.totalThoughts,
|
||||
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
|
||||
thoughtNumber: input.thoughtNumber,
|
||||
totalThoughts: input.totalThoughts,
|
||||
nextThoughtNeeded: input.nextThoughtNeeded,
|
||||
branches: Object.keys(this.branches),
|
||||
thoughtHistoryLength: this.thoughtHistory.length
|
||||
}, null, 2)
|
||||
@@ -115,7 +86,7 @@ export class SequentialThinkingServer {
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
status: 'failed'
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext"
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"vitest.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user