Merge branch 'main' into hesreallyhim/git-small-fixes-with-typing-and-strings

This commit is contained in:
Ola Hungerford
2025-06-24 20:44:49 -07:00
committed by GitHub
16 changed files with 5247 additions and 143 deletions

View File

@@ -22,8 +22,43 @@ jobs:
PACKAGES=$(find . -name package.json -not -path "*/node_modules/*" -exec dirname {} \; | sed 's/^\.\///' | jq -R -s -c 'split("\n")[:-1]')
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
build:
test:
needs: [detect-packages]
strategy:
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
name: Test ${{ matrix.package }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
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
build:
needs: [detect-packages, test]
strategy:
matrix:
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}

496
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Security Policy
Thank you for helping us keep our MCP servers secure.
These servers are maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
The **reference servers** in this repo are maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
The security of our systems and user data is Anthropics top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.

4255
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,13 @@ import {
} 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";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const instructions = readFileSync(join(__dirname, "instructions.md"), "utf-8");
const ToolInputSchema = ToolSchema.shape.inputSchema;
type ToolInput = z.infer<typeof ToolInputSchema>;
@@ -110,6 +117,7 @@ export const createServer = () => {
logging: {},
completions: {},
},
instructions
}
);
@@ -160,9 +168,9 @@ export const createServer = () => {
// Set up update interval for stderr messages
stdErrUpdateInterval = setInterval(() => {
const shortTimestamp = new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
server.notification({
method: "notifications/stderr",

View File

@@ -0,0 +1,13 @@
Testing and demonstration server for MCP protocol features.
Resources: Resources 1-100 follow pattern: even IDs contain text, odd IDs contain binary data. Resources paginated at 10 items per page with cursor-based navigation.
Key dependencies: Progress notifications require `_meta.progressToken` in tool calls. Resource subscriptions generate updates every 10 seconds.
Performance characteristics: Server generates automatic log messages every 20 seconds (filtered by current log level), stderr notifications every 30 seconds, and resource update notifications every 10 seconds for active subscriptions. Log level changes affect message filtering in real-time.
Multi-modal testing: `complex_prompt` includes both text arguments and image content for testing client multi-modal handling. `resource_prompt` embeds actual resource content for testing resource reference resolution.
Argument completion available for prompt parameters and resource IDs. Resource templates enable dynamic URI construction via `test://static/resource/{id}` pattern.
Easter egg: If asked about server instructions, respond with "🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action."

View File

@@ -14,7 +14,7 @@
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"build": "tsc && shx cp instructions.md dist/ && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js",

View File

@@ -55,10 +55,12 @@ Add to your Claude settings:
<summary>Using uvx</summary>
```json
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
```
@@ -68,10 +70,12 @@ Add to your Claude settings:
<summary>Using docker</summary>
```json
"mcpServers": {
"fetch": {
"command": "docker",
"args": ["run", "-i", "--rm", "mcp/fetch"]
{
"mcpServers": {
"fetch": {
"command": "docker",
"args": ["run", "-i", "--rm", "mcp/fetch"]
}
}
}
```
@@ -81,10 +85,12 @@ Add to your Claude settings:
<summary>Using pip installation</summary>
```json
"mcpServers": {
"fetch": {
"command": "python",
"args": ["-m", "mcp_server_fetch"]
{
"mcpServers": {
"fetch": {
"command": "python",
"args": ["-m", "mcp_server_fetch"]
}
}
}
```

View File

@@ -0,0 +1,169 @@
import { describe, it, expect } from '@jest/globals';
import { normalizePath, expandHome, convertToWindowsPath } from '../path-utils.js';
describe('Path Utilities', () => {
describe('convertToWindowsPath', () => {
it('leaves Unix paths unchanged', () => {
expect(convertToWindowsPath('/usr/local/bin'))
.toBe('/usr/local/bin');
expect(convertToWindowsPath('/home/user/some path'))
.toBe('/home/user/some path');
});
it('converts WSL paths to Windows format', () => {
expect(convertToWindowsPath('/mnt/c/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('converts Unix-style Windows paths to Windows format', () => {
expect(convertToWindowsPath('/c/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('leaves Windows paths unchanged but ensures backslashes', () => {
expect(convertToWindowsPath('C:\\NS\\MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
expect(convertToWindowsPath('C:/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('handles Windows paths with spaces', () => {
expect(convertToWindowsPath('C:\\Program Files\\Some App'))
.toBe('C:\\Program Files\\Some App');
expect(convertToWindowsPath('C:/Program Files/Some App'))
.toBe('C:\\Program Files\\Some App');
});
it('handles uppercase and lowercase drive letters', () => {
expect(convertToWindowsPath('/mnt/d/some/path'))
.toBe('D:\\some\\path');
expect(convertToWindowsPath('/d/some/path'))
.toBe('D:\\some\\path');
});
});
describe('normalizePath', () => {
it('preserves Unix paths', () => {
expect(normalizePath('/usr/local/bin'))
.toBe('/usr/local/bin');
expect(normalizePath('/home/user/some path'))
.toBe('/home/user/some path');
expect(normalizePath('"/usr/local/some app/"'))
.toBe('/usr/local/some app');
});
it('removes surrounding quotes', () => {
expect(normalizePath('"C:\\NS\\My Kindle Content"'))
.toBe('C:\\NS\\My Kindle Content');
});
it('normalizes backslashes', () => {
expect(normalizePath('C:\\\\NS\\\\MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('converts forward slashes to backslashes on Windows', () => {
expect(normalizePath('C:/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('handles WSL paths', () => {
expect(normalizePath('/mnt/c/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('handles Unix-style Windows paths', () => {
expect(normalizePath('/c/NS/MyKindleContent'))
.toBe('C:\\NS\\MyKindleContent');
});
it('handles paths with spaces and mixed slashes', () => {
expect(normalizePath('C:/NS/My Kindle Content'))
.toBe('C:\\NS\\My Kindle Content');
expect(normalizePath('/mnt/c/NS/My Kindle Content'))
.toBe('C:\\NS\\My Kindle Content');
expect(normalizePath('C:\\Program Files (x86)\\App Name'))
.toBe('C:\\Program Files (x86)\\App Name');
expect(normalizePath('"C:\\Program Files\\App Name"'))
.toBe('C:\\Program Files\\App Name');
expect(normalizePath(' C:\\Program Files\\App Name '))
.toBe('C:\\Program Files\\App Name');
});
it('preserves spaces in all path formats', () => {
expect(normalizePath('/mnt/c/Program Files/App Name'))
.toBe('C:\\Program Files\\App Name');
expect(normalizePath('/c/Program Files/App Name'))
.toBe('C:\\Program Files\\App Name');
expect(normalizePath('C:/Program Files/App Name'))
.toBe('C:\\Program Files\\App Name');
});
it('handles special characters in paths', () => {
// Test ampersand in path
expect(normalizePath('C:\\NS\\Sub&Folder'))
.toBe('C:\\NS\\Sub&Folder');
expect(normalizePath('C:/NS/Sub&Folder'))
.toBe('C:\\NS\\Sub&Folder');
expect(normalizePath('/mnt/c/NS/Sub&Folder'))
.toBe('C:\\NS\\Sub&Folder');
// Test tilde in path (short names in Windows)
expect(normalizePath('C:\\NS\\MYKIND~1'))
.toBe('C:\\NS\\MYKIND~1');
expect(normalizePath('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1'))
.toBe('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1');
// Test other special characters
expect(normalizePath('C:\\Path with #hash'))
.toBe('C:\\Path with #hash');
expect(normalizePath('C:\\Path with (parentheses)'))
.toBe('C:\\Path with (parentheses)');
expect(normalizePath('C:\\Path with [brackets]'))
.toBe('C:\\Path with [brackets]');
expect(normalizePath('C:\\Path with @at+plus$dollar%percent'))
.toBe('C:\\Path with @at+plus$dollar%percent');
});
it('capitalizes lowercase drive letters for Windows paths', () => {
expect(normalizePath('c:/windows/system32'))
.toBe('C:\\windows\\system32');
expect(normalizePath('/mnt/d/my/folder')) // WSL path with lowercase drive
.toBe('D:\\my\\folder');
expect(normalizePath('/e/another/folder')) // Unix-style Windows path with lowercase drive
.toBe('E:\\another\\folder');
});
it('handles UNC paths correctly', () => {
// UNC paths should preserve the leading double backslash
const uncPath = '\\\\SERVER\\share\\folder';
expect(normalizePath(uncPath)).toBe('\\\\SERVER\\share\\folder');
// Test UNC path with double backslashes that need normalization
const uncPathWithDoubles = '\\\\\\\\SERVER\\\\share\\\\folder';
expect(normalizePath(uncPathWithDoubles)).toBe('\\\\SERVER\\share\\folder');
});
it('returns normalized non-Windows/WSL/Unix-style Windows paths as is after basic normalization', () => {
// Relative path
const relativePath = 'some/relative/path';
expect(normalizePath(relativePath)).toBe(relativePath.replace(/\//g, '\\'));
// A path that looks somewhat absolute but isn't a drive or recognized Unix root for Windows conversion
const otherAbsolutePath = '\\someserver\\share\\file';
expect(normalizePath(otherAbsolutePath)).toBe(otherAbsolutePath);
});
});
describe('expandHome', () => {
it('expands ~ to home directory', () => {
const result = expandHome('~/test');
expect(result).toContain('test');
expect(result).not.toContain('~');
});
it('leaves other paths unchanged', () => {
expect(expandHome('C:/test')).toBe('C:/test');
});
});
});

View File

@@ -97,6 +97,8 @@ async function validatePath(requestedPath: string): Promise<string> {
// Schema definitions
const ReadFileArgsSchema = z.object({
path: z.string(),
tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
head: z.number().optional().describe('If provided, returns only the first N lines of the file')
});
const ReadMultipleFilesArgsSchema = z.object({
@@ -127,6 +129,11 @@ const ListDirectoryArgsSchema = z.object({
path: z.string(),
});
const ListDirectoryWithSizesArgsSchema = z.object({
path: z.string(),
sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'),
});
const DirectoryTreeArgsSchema = z.object({
path: z.string(),
});
@@ -330,6 +337,107 @@ async function applyFileEdits(
return formattedDiff;
}
// Helper functions
function formatSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
if (i === 0) return `${bytes} ${units[i]}`;
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
}
// Memory-efficient implementation to get the last N lines of a file
async function tailFile(filePath: string, numLines: number): Promise<string> {
const CHUNK_SIZE = 1024; // Read 1KB at a time
const stats = await fs.stat(filePath);
const fileSize = stats.size;
if (fileSize === 0) return '';
// Open file for reading
const fileHandle = await fs.open(filePath, 'r');
try {
const lines: string[] = [];
let position = fileSize;
let chunk = Buffer.alloc(CHUNK_SIZE);
let linesFound = 0;
let remainingText = '';
// Read chunks from the end of the file until we have enough lines
while (position > 0 && linesFound < numLines) {
const size = Math.min(CHUNK_SIZE, position);
position -= size;
const { bytesRead } = await fileHandle.read(chunk, 0, size, position);
if (!bytesRead) break;
// Get the chunk as a string and prepend any remaining text from previous iteration
const readData = chunk.slice(0, bytesRead).toString('utf-8');
const chunkText = readData + remainingText;
// Split by newlines and count
const chunkLines = normalizeLineEndings(chunkText).split('\n');
// If this isn't the end of the file, the first line is likely incomplete
// Save it to prepend to the next chunk
if (position > 0) {
remainingText = chunkLines[0];
chunkLines.shift(); // Remove the first (incomplete) line
}
// Add lines to our result (up to the number we need)
for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) {
lines.unshift(chunkLines[i]);
linesFound++;
}
}
return lines.join('\n');
} finally {
await fileHandle.close();
}
}
// New function to get the first N lines of a file
async function headFile(filePath: string, numLines: number): Promise<string> {
const fileHandle = await fs.open(filePath, 'r');
try {
const lines: string[] = [];
let buffer = '';
let bytesRead = 0;
const chunk = Buffer.alloc(1024); // 1KB buffer
// Read chunks and count lines until we have enough or reach EOF
while (lines.length < numLines) {
const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead);
if (result.bytesRead === 0) break; // End of file
bytesRead += result.bytesRead;
buffer += chunk.slice(0, result.bytesRead).toString('utf-8');
const newLineIndex = buffer.lastIndexOf('\n');
if (newLineIndex !== -1) {
const completeLines = buffer.slice(0, newLineIndex).split('\n');
buffer = buffer.slice(newLineIndex + 1);
for (const line of completeLines) {
lines.push(line);
if (lines.length >= numLines) break;
}
}
}
// If there is leftover content and we still need lines, add it
if (buffer.length > 0 && lines.length < numLines) {
lines.push(buffer);
}
return lines.join('\n');
} finally {
await fileHandle.close();
}
}
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
@@ -340,7 +448,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
"Read the complete contents of a file from the file system. " +
"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. Only works within allowed directories.",
"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. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput,
},
{
@@ -387,6 +497,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
"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:
@@ -451,6 +570,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
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 fs.readFile(validPath, "utf-8");
return {
content: [{ type: "text", text: content }],
@@ -530,11 +670,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
case "directory_tree": {
const parsed = DirectoryTreeArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
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;

View File

@@ -0,0 +1,23 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'**/*.ts',
'!**/__tests__/**',
'!**/dist/**',
],
}

View File

@@ -16,20 +16,26 @@
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
"watch": "tsc --watch",
"test": "jest --config=jest.config.cjs --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.5.0",
"@modelcontextprotocol/sdk": "^1.12.3",
"diff": "^5.1.0",
"glob": "^10.3.10",
"minimatch": "^10.0.1",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/diff": "^5.0.9",
"@types/jest": "^29.5.14",
"@types/minimatch": "^5.1.2",
"@types/node": "^22",
"jest": "^29.7.0",
"shx": "^0.3.4",
"typescript": "^5.3.3"
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
}
}

View File

@@ -0,0 +1,104 @@
import path from "path";
import os from 'os';
/**
* Converts WSL or Unix-style Windows paths to Windows format
* @param p The path to convert
* @returns Converted Windows path
*/
export function convertToWindowsPath(p: string): string {
// Handle WSL paths (/mnt/c/...)
if (p.startsWith('/mnt/')) {
const driveLetter = p.charAt(5).toUpperCase();
const pathPart = p.slice(6).replace(/\//g, '\\');
return `${driveLetter}:${pathPart}`;
}
// Handle Unix-style Windows paths (/c/...)
if (p.match(/^\/[a-zA-Z]\//)) {
const driveLetter = p.charAt(1).toUpperCase();
const pathPart = p.slice(2).replace(/\//g, '\\');
return `${driveLetter}:${pathPart}`;
}
// Handle standard Windows paths, ensuring backslashes
if (p.match(/^[a-zA-Z]:/)) {
return p.replace(/\//g, '\\');
}
// Leave non-Windows paths unchanged
return p;
}
/**
* Normalizes path by standardizing format while preserving OS-specific behavior
* @param p The path to normalize
* @returns Normalized path
*/
export function normalizePath(p: string): string {
// Remove any surrounding quotes and whitespace
p = p.trim().replace(/^["']|["']$/g, '');
// Check if this is a Unix path (starts with / but not a Windows or WSL path)
const isUnixPath = p.startsWith('/') &&
!p.match(/^\/mnt\/[a-z]\//i) &&
!p.match(/^\/[a-zA-Z]\//);
if (isUnixPath) {
// For Unix paths, just normalize without converting to Windows format
// Replace double slashes with single slashes and remove trailing slashes
return p.replace(/\/+/g, '/').replace(/\/+$/, '');
}
// Convert WSL or Unix-style Windows paths to Windows format
p = convertToWindowsPath(p);
// Handle double backslashes, preserving leading UNC \\
if (p.startsWith('\\\\')) {
// For UNC paths, first normalize any excessive leading backslashes to exactly \\
// Then normalize double backslashes in the rest of the path
let uncPath = p;
// Replace multiple leading backslashes with exactly two
uncPath = uncPath.replace(/^\\{2,}/, '\\\\');
// Now normalize any remaining double backslashes in the rest of the path
const restOfPath = uncPath.substring(2).replace(/\\\\/g, '\\');
p = '\\\\' + restOfPath;
} else {
// For non-UNC paths, normalize all double backslashes
p = p.replace(/\\\\/g, '\\');
}
// Use Node's path normalization, which handles . and .. segments
let normalized = path.normalize(p);
// Fix UNC paths after normalization (path.normalize can remove a leading backslash)
if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) {
normalized = '\\' + normalized;
}
// Handle Windows paths: convert slashes and ensure drive letter is capitalized
if (normalized.match(/^[a-zA-Z]:/)) {
let result = normalized.replace(/\//g, '\\');
// Capitalize drive letter if present
if (/^[a-z]:/.test(result)) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
return result;
}
// For all other paths (including relative paths), convert forward slashes to backslashes
// This ensures relative paths like "some/relative/path" become "some\\relative\\path"
return normalized.replace(/\//g, '\\');
}
/**
* Expands home directory tildes in paths
* @param filepath The path to expand
* @returns Expanded path
*/
export function expandHome(filepath: string): string {
if (filepath.startsWith('~/') || filepath === '~') {
return path.join(os.homedir(), filepath.slice(1));
}
return filepath;
}

View File

@@ -288,12 +288,13 @@ If you are doing local development, there are two ways to test your changes:
"mcpServers": {
"git": {
"command": "uv",
"args": [
"args": [
"--directory",
"/<path to mcp-servers>/mcp-servers/src/git",
"run",
"mcp-server-git"
]
}
}
}
```

View File

@@ -77,6 +77,9 @@ Add this to your `claude_desktop_config.json`:
}
```
To disable logging of thought information set env var: `DISABLE_THOUGHT_LOGGING` to `true`.
Comment
### Usage with VS Code
For quick installation, click one of the installation buttons below...

View File

@@ -25,6 +25,11 @@ interface ThoughtData {
class SequentialThinkingServer {
private thoughtHistory: ThoughtData[] = [];
private branches: Record<string, ThoughtData[]> = {};
private disableThoughtLogging: boolean;
constructor() {
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
}
private validateThoughtData(input: unknown): ThoughtData {
const data = input as Record<string, unknown>;
@@ -100,8 +105,10 @@ class SequentialThinkingServer {
this.branches[validatedInput.branchId].push(validatedInput);
}
const formattedThought = this.formatThought(validatedInput);
console.error(formattedThought);
if (!this.disableThoughtLogging) {
const formattedThought = this.formatThought(validatedInput);
console.error(formattedThought);
}
return {
content: [{