Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 67 additions & 23 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import subprocess
from dataclasses import dataclass
from typing import Any, Iterator, NewType, Protocol
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache


Version = NewType("Version", str)
Expand Down Expand Up @@ -62,47 +64,61 @@ def update_version(self, version: Version) -> None: ...
@dataclass
class NpmPackage:
path: Path
_name_cache: str | None = None

def package_name(self) -> str:
with open(self.path / "package.json", "r") as f:
return json.load(f)["name"]
if self._name_cache is None:
with open(self.path / "package.json", "r", encoding="utf-8") as f:
self._name_cache = json.load(f)["name"]
return self._name_cache

def update_version(self, version: Version):
with open(self.path / "package.json", "r+") as f:
package_json_path = self.path / "package.json"
with open(package_json_path, "r+", encoding="utf-8") as f:
data = json.load(f)
data["version"] = version
f.seek(0)
json.dump(data, f, indent=2)
json.dump(data, f, indent=2, ensure_ascii=False)
f.truncate()


@dataclass
class PyPiPackage:
path: Path
_name_cache: str | None = None

def package_name(self) -> str:
with open(self.path / "pyproject.toml") as f:
toml_data = tomlkit.parse(f.read())
name = toml_data.get("project", {}).get("name")
if not name:
raise Exception("No name in pyproject.toml project section")
return str(name)
if self._name_cache is None:
pyproject_path = self.path / "pyproject.toml"
with open(pyproject_path, "r", encoding="utf-8") as f:
toml_data = tomlkit.parse(f.read())
name = toml_data.get("project", {}).get("name")
if not name:
raise ValueError(f"No name in pyproject.toml project section for {self.path}")
self._name_cache = str(name)
return self._name_cache

def update_version(self, version: Version):
pyproject_path = self.path / "pyproject.toml"

# Update version in pyproject.toml
with open(self.path / "pyproject.toml") as f:
with open(pyproject_path, "r", encoding="utf-8") as f:
data = tomlkit.parse(f.read())
data["project"]["version"] = version

with open(self.path / "pyproject.toml", "w") as f:
with open(pyproject_path, "w", encoding="utf-8") as f:
f.write(tomlkit.dumps(data))

# Regenerate uv.lock to match the updated pyproject.toml
subprocess.run(["uv", "lock"], cwd=self.path, check=True)
subprocess.run(["uv", "lock"], cwd=self.path, check=True, capture_output=True)


def has_changes(path: Path, git_hash: GitHash) -> bool:
@lru_cache(maxsize=128)
def has_changes(path_str: str, git_hash_str: str) -> bool:
"""Check if any files changed between current state and git hash"""
path = Path(path_str)
git_hash = GitHash(git_hash_str)

try:
output = subprocess.run(
["git", "diff", "--name-only", git_hash, "--", "."],
Expand All @@ -112,9 +128,9 @@ def has_changes(path: Path, git_hash: GitHash) -> bool:
text=True,
)

changed_files = [Path(f) for f in output.stdout.splitlines()]
relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts"]]
return len(relevant_files) >= 1
changed_files = output.stdout.splitlines()
# Use any() for early exit
return any(f.endswith(('.py', '.ts')) for f in changed_files)
except subprocess.CalledProcessError:
return False

Expand All @@ -126,12 +142,34 @@ def gen_version() -> Version:


def find_changed_packages(directory: Path, git_hash: GitHash) -> Iterator[Package]:
git_hash_str = str(git_hash)

# Collect all potential packages first
potential_packages = []

for path in directory.glob("*/package.json"):
if has_changes(path.parent, git_hash):
yield NpmPackage(path.parent)
# if has_changes(path.parent, git_hash):
# yield NpmPackage(path.parent)
potential_packages.append((path.parent, NpmPackage))


for path in directory.glob("*/pyproject.toml"):
if has_changes(path.parent, git_hash):
yield PyPiPackage(path.parent)
# if has_changes(path.parent, git_hash):
# yield PyPiPackage(path.parent)
potential_packages.append((path.parent, PyPiPackage))

# Check changes in parallel for better performance
with ThreadPoolExecutor(max_workers=min(4, len(potential_packages))) as executor:
def check_and_create(pkg_path, pkg_class):
if has_changes(str(pkg_path), git_hash_str):
return pkg_class(pkg_path)
return None

results = executor.map(lambda args: check_and_create(*args), potential_packages)

for result in results:
if result is not None:
yield result


@click.group()
Expand Down Expand Up @@ -195,14 +233,20 @@ def generate_version() -> int:
def generate_matrix(directory: Path, git_hash: GitHash, pypi: bool, npm: bool) -> int:
# Detect package type
path = directory.resolve(strict=True)
version = gen_version()
# version = gen_version()

# Early exit if neither flag is set
if not npm and not pypi:
click.echo(json.dumps([]))
return 0

changes = []
for package in find_changed_packages(path, git_hash):
pkg = package.path.relative_to(path)
if npm and isinstance(package, NpmPackage):
changes.append(str(pkg))
if pypi and isinstance(package, PyPiPackage):
# if pypi and isinstance(package, PyPiPackage):
elif pypi and isinstance(package, PyPiPackage): # Use elif for efficiency
changes.append(str(pkg))

click.echo(json.dumps(changes))
Expand Down
34 changes: 34 additions & 0 deletions src/everything/__tests__/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createServer } from '../server/index.js';

describe('Server Logging', () => {
let consoleSpy: { error: any };

beforeEach(() => {
consoleSpy = {
error: vi.spyOn(console, 'error').mockImplementation(() => { })
};
});

afterEach(() => {
consoleSpy.error.mockRestore();
});

describe('createServer', () => {
it('should initialize without logging errors', () => {
const { server } = createServer();

expect(server).toBeDefined();
expect(consoleSpy.error).not.toHaveBeenCalled();
});

it('should handle multiple server creations', () => {
const { server: server1 } = createServer();
const { server: server2 } = createServer();

expect(server1).toBeDefined();
expect(server2).toBeDefined();
expect(server1).not.toBe(server2);
});
});
});
5 changes: 3 additions & 2 deletions src/everything/__tests__/registrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe('Registration Index Files', () => {

registerTools(mockServer);

// Should register 12 standard tools (non-conditional)
expect(mockServer.registerTool).toHaveBeenCalledTimes(12);
// Should register 13 standard tools (non-conditional)
expect(mockServer.registerTool).toHaveBeenCalledTimes(13);

// Verify specific tools are registered
const registeredTools = (mockServer.registerTool as any).mock.calls.map(
Expand All @@ -36,6 +36,7 @@ describe('Registration Index Files', () => {
expect(registeredTools).toContain('get-env');
expect(registeredTools).toContain('get-tiny-image');
expect(registeredTools).toContain('get-structured-content');
expect(registeredTools).toContain('file-operations');
expect(registeredTools).toContain('get-annotated-message');
expect(registeredTools).toContain('trigger-long-running-operation');
expect(registeredTools).toContain('get-resource-links');
Expand Down
80 changes: 80 additions & 0 deletions src/everything/__tests__/roots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { roots } from '../server/roots.js';
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

describe('Roots Module', () => {
let mockServer: any;
let consoleSpy: any;

beforeEach(() => {
mockServer = {
server: {
getClientCapabilities: vi.fn().mockReturnValue({ roots: true }),
setNotificationHandler: vi.fn(),
listRoots: vi.fn().mockResolvedValue({ roots: [] })
},
request: vi.fn().mockResolvedValue({ roots: [] }),
setNotificationHandler: vi.fn(),
sendLoggingMessage: vi.fn()
};
consoleSpy = {
error: vi.spyOn(console, 'error').mockImplementation(() => { })
};
roots.clear();
});

afterEach(() => {
consoleSpy.error.mockRestore();
vi.clearAllMocks();
});

describe('Roots Management', () => {
it('should initialize empty roots map', () => {
expect(roots.size).toBe(0);
});

it('should store roots by session ID', async () => {
const sessionId = 'test-session';
// const testRoots = [{ uri: 'file:///test' }];
const testRoots: any[] = [];

mockServer.request.mockResolvedValue({ roots: testRoots });

// Import and call the function to sync roots
const { syncRoots } = await import('../server/roots.js');
await syncRoots(mockServer, sessionId);

expect(roots.has(sessionId)).toBe(true);
expect(roots.get(sessionId)).toEqual(testRoots);
});

it('should handle missing session gracefully', async () => {
const sessionId = 'non-existent-session';

const { syncRoots } = await import('../server/roots.js');

// When session doesn't exist, should return empty array
const result = roots.get(sessionId);
expect(result).toBeUndefined();
});
});

// describe('Error Handling', () => {
// it('should log errors when request fails', async () => {
// const sessionId = 'test-session';
// const testError = new Error('Request failed');

// mockServer.request.mockRejectedValue(testError);

// const { syncRoots } = await import('../server/roots.js');
// await syncRoots(mockServer, sessionId);

// // console.log('consoleSpy', consoleSpy)
// // expect(consoleSpy.error()).toHaveBeenCalled();
// expect(consoleSpy.error()).toHaveBeenCalledWith(
// 'Failed to request roots from client:',
// testError
// );
// });
// });
});
56 changes: 56 additions & 0 deletions src/everything/__tests__/transports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createServer } from '../server/index.js';

describe('Transport Layer', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('StreamableHTTP Transport', () => {
it('should have required exports', () => {
expect(typeof createServer).toBe('function');
});

it('should handle initialization', () => {
const { server } = createServer();
expect(server).toBeDefined();
expect(server.server).toBeDefined();
});
});

describe('SSE Transport', () => {
it('should have required exports', () => {
expect(typeof createServer).toBe('function');
});

it('should initialize server components', () => {
const { server, cleanup } = createServer();
expect(server).toBeDefined();
expect(typeof cleanup).toBe('function');
});
});

describe('STDIO Transport', () => {
it('should have required exports', () => {
expect(typeof createServer).toBe('function');
});

it('should handle stdio initialization', () => {
const { server } = createServer();
expect(server).toBeDefined();
});
});

describe('Transport Error Handling', () => {
it('should handle server creation errors gracefully', () => {
vi.spyOn(console, 'error').mockImplementation(() => { });

try {
const { server } = createServer();
expect(server).toBeDefined();
} catch (error) {
expect(error).toBeDefined();
}
});
});
});
5 changes: 2 additions & 3 deletions src/everything/server/roots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const roots: Map<string | undefined, Root[]> = new Map<
* @throws {Error} In case of a failure to request the roots from the client, an error log message is sent.
*/
export const syncRoots = async (server: McpServer, sessionId?: string) => {
const clientCapabilities = server.server.getClientCapabilities() || {};
const clientCapabilities = server.server?.getClientCapabilities() || {};
const clientSupportsRoots: boolean = clientCapabilities?.roots !== undefined;

// Fetch the roots list for this client
Expand Down Expand Up @@ -64,8 +64,7 @@ export const syncRoots = async (server: McpServer, sessionId?: string) => {
}
} catch (error) {
console.error(
`Failed to request roots from client ${sessionId}: ${
error instanceof Error ? error.message : String(error)
`Failed to request roots from client ${sessionId}: ${error instanceof Error ? error.message : String(error)
}`
);
}
Expand Down
Loading