Merge remote-tracking branch 'upstream/main' into add-sequentialthinking-tests-update-sdk

This commit is contained in:
olaservo 2025-10-12 18:09:41 -07:00
commit f6fbfc4c5e
11 changed files with 430 additions and 191 deletions

View file

@ -1,10 +1,13 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import {
CallToolRequestSchema,
ClientCapabilities,
CompleteRequestSchema,
CreateMessageRequest,
CreateMessageResultSchema,
ElicitRequest,
ElicitResultSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
@ -14,18 +17,20 @@ import {
ReadResourceRequestSchema,
Resource,
RootsListChangedNotificationSchema,
SetLevelRequestSchema,
ServerNotification,
ServerRequest,
SubscribeRequestSchema,
Tool,
ToolSchema,
UnsubscribeRequestSchema,
type Root,
type Root
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import JSZip from "jszip";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -37,6 +42,8 @@ type ToolInput = z.infer<typeof ToolInputSchema>;
const ToolOutputSchema = ToolSchema.shape.outputSchema;
type ToolOutput = z.infer<typeof ToolOutputSchema>;
type SendRequest = RequestHandlerExtra<ServerRequest, ServerNotification>["sendRequest"];
/* Input schemas for tools implemented in this server */
const EchoSchema = z.object({
message: z.string().describe("Message to echo"),
@ -123,6 +130,10 @@ const StructuredContentSchema = {
})
};
const ZipResourcesInputSchema = z.object({
files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"),
});
enum ToolName {
ECHO = "echo",
ADD = "add",
@ -135,6 +146,7 @@ enum ToolName {
ELICITATION = "startElicitation",
GET_RESOURCE_LINKS = "getResourceLinks",
STRUCTURED_CONTENT = "structuredContent",
ZIP_RESOURCES = "zip",
LIST_ROOTS = "listRoots"
}
@ -174,7 +186,6 @@ export const createServer = () => {
let subsUpdateInterval: NodeJS.Timeout | undefined;
let stdErrUpdateInterval: NodeJS.Timeout | undefined;
let logLevel: LoggingLevel = "debug";
let logsUpdateInterval: NodeJS.Timeout | undefined;
// Store client capabilities
let clientCapabilities: ClientCapabilities | undefined;
@ -182,55 +193,48 @@ export const createServer = () => {
// Roots state management
let currentRoots: Root[] = [];
let clientSupportsRoots = false;
const messages = [
{ level: "debug", data: "Debug-level message" },
{ level: "info", data: "Info-level message" },
{ level: "notice", data: "Notice-level message" },
{ level: "warning", data: "Warning-level message" },
{ level: "error", data: "Error-level message" },
{ level: "critical", data: "Critical-level message" },
{ level: "alert", data: "Alert level-message" },
{ level: "emergency", data: "Emergency-level message" },
];
let sessionId: string | undefined;
const isMessageIgnored = (level: LoggingLevel): boolean => {
const currentLevel = messages.findIndex((msg) => logLevel === msg.level);
const messageLevel = messages.findIndex((msg) => level === msg.level);
return messageLevel < currentLevel;
};
// Function to start notification intervals when a client connects
const startNotificationIntervals = (sid?: string|undefined) => {
sessionId = sid;
if (!subsUpdateInterval) {
subsUpdateInterval = setInterval(() => {
for (const uri of subscriptions) {
server.notification({
method: "notifications/resources/updated",
params: { uri },
});
}
}, 10000);
}
// Function to start notification intervals when a client connects
const startNotificationIntervals = () => {
if (!subsUpdateInterval) {
subsUpdateInterval = setInterval(() => {
for (const uri of subscriptions) {
server.notification({
method: "notifications/resources/updated",
params: { uri },
});
}
}, 10000);
}
const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}`: "";
const messages: { level: LoggingLevel; data: string }[] = [
{ level: "debug", data: `Debug-level message${maybeAppendSessionId}` },
{ level: "info", data: `Info-level message${maybeAppendSessionId}` },
{ level: "notice", data: `Notice-level message${maybeAppendSessionId}` },
{ level: "warning", data: `Warning-level message${maybeAppendSessionId}` },
{ level: "error", data: `Error-level message${maybeAppendSessionId}` },
{ level: "critical", data: `Critical-level message${maybeAppendSessionId}` },
{ level: "alert", data: `Alert level-message${maybeAppendSessionId}` },
{ level: "emergency", data: `Emergency-level message${maybeAppendSessionId}` },
];
if (!logsUpdateInterval) {
logsUpdateInterval = setInterval(() => {
let message = {
method: "notifications/message",
params: messages[Math.floor(Math.random() * messages.length)],
};
if (!isMessageIgnored(message.params.level as LoggingLevel))
server.notification(message);
}, 20000);
if (!logsUpdateInterval) {
console.error("Starting logs update interval");
logsUpdateInterval = setInterval(async () => {
await server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)], sessionId);
}, 15000);
}
};
// Helper method to request sampling from client
const requestSampling = async (
context: string,
uri: string,
maxTokens: number = 100
maxTokens: number = 100,
sendRequest: SendRequest
) => {
const request: CreateMessageRequest = {
method: "sampling/createMessage",
@ -251,22 +255,24 @@ export const createServer = () => {
},
};
return await server.request(request, CreateMessageResultSchema);
return await sendRequest(request, CreateMessageResultSchema);
};
const requestElicitation = async (
message: string,
requestedSchema: any
requestedSchema: any,
sendRequest: SendRequest
) => {
const request = {
const request: ElicitRequest = {
method: 'elicitation/create',
params: {
message,
requestedSchema
}
requestedSchema,
},
};
return await server.request(request, z.any());
return await sendRequest(request, ElicitResultSchema);
};
const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => {
@ -344,12 +350,9 @@ export const createServer = () => {
throw new Error(`Unknown resource: ${uri}`);
});
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => {
const { uri } = request.params;
subscriptions.add(uri);
// Request sampling from client when someone subscribes
await requestSampling("A new subscription was started", uri);
return {};
});
@ -535,6 +538,11 @@ export const createServer = () => {
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,
@ -551,7 +559,7 @@ export const createServer = () => {
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
server.setRequestHandler(CallToolRequestSchema, async (request,extra) => {
const { name, arguments: args } = request.params;
if (name === ToolName.ECHO) {
@ -593,7 +601,7 @@ export const createServer = () => {
total: steps,
progressToken,
},
});
},{relatedRequestId: extra.requestId});
}
}
@ -625,7 +633,8 @@ export const createServer = () => {
const result = await requestSampling(
prompt,
ToolName.SAMPLE_LLM,
maxTokens
maxTokens,
extra.sendRequest
);
return {
content: [
@ -744,14 +753,20 @@ export const createServer = () => {
type: 'object',
properties: {
color: { type: 'string', description: 'Favorite color' },
number: { type: 'integer', description: 'Favorite number', minimum: 1, maximum: 100 },
number: {
type: 'integer',
description: 'Favorite number',
minimum: 1,
maximum: 100,
},
pets: {
type: 'string',
enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'],
description: 'Favorite pets'
description: 'Favorite pets',
},
}
}
},
},
extra.sendRequest
);
// Handle different response actions
@ -840,6 +855,37 @@ export const createServer = () => {
};
}
if (name === ToolName.ZIP_RESOURCES) {
const { files } = ZipResourcesInputSchema.parse(args);
const zip = new JSZip();
for (const [fileName, fileUrl] of Object.entries(files)) {
try {
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
zip.file(fileName, arrayBuffer);
} catch (error) {
throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`;
return {
content: [
{
type: "resource_link",
mimeType: "application/zip",
uri,
},
],
};
}
if (name === ToolName.LIST_ROOTS) {
ListRootsSchema.parse(args);
@ -918,23 +964,6 @@ export const createServer = () => {
throw new Error(`Unknown reference type`);
});
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
const { level } = request.params;
logLevel = level;
// Demonstrate different log levels
await server.notification({
method: "notifications/message",
params: {
level: "debug",
logger: "test-server",
data: `Logging level set to: ${logLevel}`,
},
});
return {};
});
// Roots protocol handlers
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
try {
@ -944,24 +973,18 @@ export const createServer = () => {
currentRoots = response.roots;
// Log the roots update for demonstration
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "info",
logger: "everything-server",
data: `Roots updated: ${currentRoots.length} root(s) received from client`,
},
});
}, sessionId);
}
} catch (error) {
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "error",
logger: "everything-server",
data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`,
},
});
}, sessionId);
}
});
@ -976,43 +999,31 @@ export const createServer = () => {
if (response && 'roots' in response) {
currentRoots = response.roots;
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "info",
logger: "everything-server",
data: `Initial roots received: ${currentRoots.length} root(s) from client`,
},
});
}, sessionId);
} else {
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "warning",
logger: "everything-server",
data: "Client returned no roots set",
},
});
}, sessionId);
}
} catch (error) {
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "error",
logger: "everything-server",
data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`,
},
});
}, sessionId);
}
} else {
await server.notification({
method: "notifications/message",
params: {
await server.sendLoggingMessage({
level: "info",
logger: "everything-server",
data: "Client does not support MCP roots protocol",
},
});
}, sessionId);
}
};

View file

@ -24,10 +24,12 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.19.1",
"express": "^4.21.1",
"jszip": "^3.10.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"shx": "^0.3.4",
"typescript": "^5.6.2"

View file

@ -1,11 +1,17 @@
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { createServer } from "./everything.js";
import cors from 'cors';
console.error('Starting SSE server...');
const app = express();
app.use(cors({
"origin": "*", // use "*" with caution in production
"methods": "GET,POST",
"preflightContinue": false,
"optionsSuccessStatus": 204,
})); // Enable CORS for all routes so Inspector can connect
const transports: Map<string, SSEServerTransport> = new Map<string, SSEServerTransport>();
app.get("/sse", async (req, res) => {
@ -26,7 +32,7 @@ app.get("/sse", async (req, res) => {
console.error("Client Connected: ", transport.sessionId);
// Start notification intervals after client connects
startNotificationIntervals();
startNotificationIntervals(transport.sessionId);
// Handle close of connection
server.onclose = async () => {

View file

@ -6,17 +6,18 @@ import { createServer } from "./everything.js";
console.error('Starting default (STDIO) server...');
async function main() {
const transport = new StdioServerTransport();
const {server, cleanup} = createServer();
const transport = new StdioServerTransport();
const {server, cleanup, startNotificationIntervals} = createServer();
await server.connect(transport);
await server.connect(transport);
startNotificationIntervals();
// Cleanup on exit
process.on("SIGINT", async () => {
await cleanup();
await server.close();
process.exit(0);
});
// Cleanup on exit
process.on("SIGINT", async () => {
await cleanup();
await server.close();
process.exit(0);
});
}
main().catch((error) => {

View file

@ -3,10 +3,22 @@ import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/in
import express, { Request, Response } from "express";
import { createServer } from "./everything.js";
import { randomUUID } from 'node:crypto';
import cors from 'cors';
console.error('Starting Streamable HTTP server...');
const app = express();
app.use(cors({
"origin": "*", // use "*" with caution in production
"methods": "GET,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204,
"exposedHeaders": [
'mcp-session-id',
'last-event-id',
'mcp-protocol-version'
]
})); // Enable CORS for all routes so Inspector can connect
const transports: Map<string, StreamableHTTPServerTransport> = new Map<string, StreamableHTTPServerTransport>();
@ -15,6 +27,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
@ -22,7 +35,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
transport = transports.get(sessionId)!;
} else if (!sessionId) {
const { server, cleanup } = createServer();
const { server, cleanup, startNotificationIntervals } = createServer();
// New initialization request
const eventStore = new InMemoryEventStore();
@ -53,7 +66,11 @@ app.post('/mcp', async (req: Request, res: Response) => {
await server.connect(transport);
await transport.handleRequest(req, res);
return; // Already handled
// Wait until initialize is complete and transport will have a sessionId
startNotificationIntervals(transport.sessionId);
return; // Already handled
} else {
// Invalid request - no session ID or not initialization request
res.status(400).json({

View file

@ -128,15 +128,6 @@ The server's directory access control follows this flow:
- Returns detailed listing with file sizes and summary statistics
- Shows total files, directories, and combined size
- **directory_tree**
- Get a recursive tree view of files and directories as a JSON structure
- Input: `path` (string): Starting directory path
- Returns JSON structure with:
- `name`: File/directory name
- `type`: "file" or "directory"
- `children`: Array of child entries (for directories only)
- Output is formatted with 2-space indentation for readability
- **move_file**
- Move or rename files and directories
- Inputs:
@ -165,6 +156,7 @@ The server's directory access control follows this flow:
- `children` (array): Present only for directories
- Empty array for empty directories
- Omitted for files
- Output is formatted with 2-space indentation for readability
- **get_file_info**
- Get detailed file/directory metadata

View file

@ -84,13 +84,8 @@ Please note that mcp-server-git is currently in early development. The functiona
- `repo_path` (string): Path to Git repository
- `revision` (string): The revision (commit hash, branch name, tag) to show
- Returns: Contents of the specified commit
12. `git_init`
- Initializes a Git repository
- Inputs:
- `repo_path` (string): Path to directory to initialize git repo
- Returns: Confirmation of repository initialization
13. `git_branch`
12. `git_branch`
- List Git branches
- Inputs:
- `repo_path` (string): Path to the Git repository.

View file

@ -70,8 +70,7 @@ class GitShow(BaseModel):
repo_path: str
revision: str
class GitInit(BaseModel):
repo_path: str
class GitBranch(BaseModel):
repo_path: str = Field(
@ -104,7 +103,7 @@ class GitTools(str, Enum):
CREATE_BRANCH = "git_create_branch"
CHECKOUT = "git_checkout"
SHOW = "git_show"
INIT = "git_init"
BRANCH = "git_branch"
def git_status(repo: git.Repo) -> str:
@ -183,12 +182,7 @@ def git_checkout(repo: git.Repo, branch_name: str) -> str:
repo.git.checkout(branch_name)
return f"Switched to branch '{branch_name}'"
def git_init(repo_path: str) -> str:
try:
repo = git.Repo.init(path=repo_path, mkdir=True)
return f"Initialized empty Git repository in {repo.git_dir}"
except Exception as e:
return f"Error initializing repository: {str(e)}"
def git_show(repo: git.Repo, revision: str) -> str:
commit = repo.commit(revision)
@ -308,11 +302,7 @@ async def serve(repository: Path | None) -> None:
description="Shows the contents of a commit",
inputSchema=GitShow.model_json_schema(),
),
Tool(
name=GitTools.INIT,
description="Initialize a new Git repository",
inputSchema=GitInit.model_json_schema(),
),
Tool(
name=GitTools.BRANCH,
description="List Git branches",
@ -354,15 +344,7 @@ async def serve(repository: Path | None) -> None:
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
repo_path = Path(arguments["repo_path"])
# Handle git init separately since it doesn't require an existing repo
if name == GitTools.INIT:
result = git_init(str(repo_path))
return [TextContent(
type="text",
text=result
)]
# For all other commands, we need an existing repo
# For all commands, we need an existing repo
repo = git.Repo(repo_path)
match name:

View file

@ -60,8 +60,18 @@ class KnowledgeGraphManager {
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
const lines = [
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
...graph.entities.map(e => JSON.stringify({
type: "entity",
name: e.name,
entityType: e.entityType,
observations: e.observations
})),
...graph.relations.map(r => JSON.stringify({
type: "relation",
from: r.from,
to: r.to,
relationType: r.relationType
})),
];
await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n"));
}
@ -219,10 +229,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
required: ["name", "entityType", "observations"],
additionalProperties: false,
},
},
},
required: ["entities"],
additionalProperties: false,
},
},
{
@ -241,10 +253,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
relationType: { type: "string", description: "The type of the relation" },
},
required: ["from", "to", "relationType"],
additionalProperties: false,
},
},
},
required: ["relations"],
additionalProperties: false,
},
},
{
@ -266,10 +280,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
required: ["entityName", "contents"],
additionalProperties: false,
},
},
},
required: ["observations"],
additionalProperties: false,
},
},
{
@ -285,6 +301,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
required: ["entityNames"],
additionalProperties: false,
},
},
{
@ -306,10 +323,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
required: ["entityName", "observations"],
additionalProperties: false,
},
},
},
required: ["deletions"],
additionalProperties: false,
},
},
{
@ -328,11 +347,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
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,
},
},
{
@ -341,6 +362,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
},
{
@ -352,6 +374,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
},
required: ["query"],
additionalProperties: false,
},
},
{
@ -367,6 +390,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
},
required: ["names"],
additionalProperties: false,
},
},
],

View file

@ -46,7 +46,8 @@ def get_local_tz(local_tz_override: str | None = None) -> ZoneInfo:
local_tzname = get_localzone_name()
if local_tzname is not None:
return ZoneInfo(local_tzname)
raise McpError("Could not determine local timezone - tzinfo is None")
# Default to UTC if local timezone cannot be determined
return ZoneInfo("UTC")
def get_zoneinfo(timezone_name: str) -> ZoneInfo:

View file

@ -486,10 +486,10 @@ def test_get_local_tz_with_valid_iana_name(mock_get_localzone):
@patch('mcp_server_time.server.get_localzone_name')
def test_get_local_tz_when_none_returned(mock_get_localzone):
"""Test error when tzlocal returns None."""
"""Test default to UTC when tzlocal returns None."""
mock_get_localzone.return_value = None
with pytest.raises(McpError, match="Could not determine local timezone"):
get_local_tz()
result = get_local_tz()
assert str(result) == "UTC"
@patch('mcp_server_time.server.get_localzone_name')