From 3e1be88c3bf20bcaf8cb007d190c708f69057fee Mon Sep 17 00:00:00 2001 From: Greg King Date: Mon, 26 Jan 2026 09:10:58 -0500 Subject: [PATCH 1/3] fix(everything): allow re-registration of session resources When a tool like `gzip-file-as-resource` is called multiple times with the same output name (especially the default `README.md.gz`), the server would throw "Resource already registered" because the SDK doesn't allow registering duplicate URIs. This fix: - Tracks registered resources by URI in a module-level Map - Before registering a new resource, checks if the URI already exists - If it does, removes the old resource using the SDK's `remove()` method - Then registers the new resource with fresh content This allows tools to be called repeatedly with the same parameters without errors, which is important for LLM agents that may retry tool calls. Found using Bellwether (https://bellwether.sh), an MCP server validation tool. Co-Authored-By: Claude Opus 4.5 --- src/everything/resources/session.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index f4e16d3b78..10e0db33c1 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -1,6 +1,13 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, RegisteredResource } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Resource, ResourceLink } from "@modelcontextprotocol/sdk/types.js"; +/** + * Tracks registered session resources by URI to allow updating/removing on re-registration. + * This prevents "Resource already registered" errors when a tool creates a resource + * with the same URI multiple times during a session. + */ +const registeredResources = new Map(); + /** * Generates a session-scoped resource URI string based on the provided resource name. * @@ -47,17 +54,27 @@ export const registerSessionResource = ( blob: payload, }; + // Check if a resource with this URI is already registered and remove it + const existingResource = registeredResources.get(uri); + if (existingResource) { + existingResource.remove(); + registeredResources.delete(uri); + } + // Register file resource - server.registerResource( + const registeredResource = server.registerResource( name, uri, { mimeType, description, title, annotations, icons, _meta }, - async (uri) => { + async () => { return { contents: [resourceContent], }; } ); + // Track the registered resource for potential future removal + registeredResources.set(uri, registeredResource); + return { type: "resource_link", ...resource }; }; From 8614dff06ff6cb0eee75af36674f1e19f035cabc Mon Sep 17 00:00:00 2001 From: thecaptain789 Date: Fri, 6 Feb 2026 15:25:43 +0000 Subject: [PATCH 2/3] fix(fetch): update to httpx 0.28+ proxy parameter The httpx library renamed 'proxies' to 'proxy' in version 0.28.0. This updates the fetch server to use the new parameter name and removes the version cap on httpx. Fixes #3287 --- src/fetch/pyproject.toml | 2 +- src/fetch/src/mcp_server_fetch/server.py | 4 ++-- src/fetch/tests/test_server.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fetch/pyproject.toml b/src/fetch/pyproject.toml index 24b42d8e3e..e2d0d38d0c 100644 --- a/src/fetch/pyproject.toml +++ b/src/fetch/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] dependencies = [ - "httpx<0.28", + "httpx>=0.27", "markdownify>=0.13.1", "mcp>=1.1.3", "protego>=0.3.1", diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index 2df9d3b604..d128987351 100644 --- a/src/fetch/src/mcp_server_fetch/server.py +++ b/src/fetch/src/mcp_server_fetch/server.py @@ -72,7 +72,7 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str, proxy_url: robot_txt_url = get_robots_txt_url(url) - async with AsyncClient(proxies=proxy_url) as client: + async with AsyncClient(proxy=proxy_url) as client: try: response = await client.get( robot_txt_url, @@ -116,7 +116,7 @@ async def fetch_url( """ from httpx import AsyncClient, HTTPError - async with AsyncClient(proxies=proxy_url) as client: + async with AsyncClient(proxy=proxy_url) as client: try: response = await client.get( url, diff --git a/src/fetch/tests/test_server.py b/src/fetch/tests/test_server.py index 10103b87c4..96c1cb38c7 100644 --- a/src/fetch/tests/test_server.py +++ b/src/fetch/tests/test_server.py @@ -323,4 +323,4 @@ async def test_fetch_with_proxy(self): ) # Verify AsyncClient was called with proxy - mock_client_class.assert_called_once_with(proxies="http://proxy.example.com:8080") + mock_client_class.assert_called_once_with(proxy="http://proxy.example.com:8080") From ca7ea2253ee0cea440cf70798b9bb93e128e400e Mon Sep 17 00:00:00 2001 From: DukeDeSouth Date: Fri, 6 Feb 2026 20:14:27 -0500 Subject: [PATCH 3/3] fix(memory): return relations connected to requested nodes in openNodes/searchNodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `openNodes` and `searchNodes` only returned relations where BOTH endpoints were in the result set (using `&&`). This silently dropped all relations to/from nodes outside the set — making it impossible to discover a node's connections without calling `read_graph` and filtering the entire dataset client-side. Changed the filter from `&&` to `||` so that any relation with at least one endpoint in the result set is included. This matches the expected graph-query semantics: when you open a node, you should see all its edges, not just edges to other opened nodes. Fixes #3137 Tests updated and new cases added covering: - Outgoing relations to nodes not in the open set - Incoming relations from nodes not in the open set - Relations connected to a single opened node - searchNodes returning outgoing relations to unmatched entities Co-authored-by: Cursor --- src/memory/__tests__/knowledge-graph.test.ts | 47 +++++++++++++++++--- src/memory/index.ts | 12 +++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 7edab5e42c..236242413a 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -302,10 +302,20 @@ describe('KnowledgeGraphManager', () => { expect(result.entities[0].name).toBe('Alice'); }); - it('should include relations between matched entities', async () => { + it('should include relations where at least one endpoint matches', async () => { const result = await manager.searchNodes('Acme'); expect(result.entities).toHaveLength(2); // Alice and Acme Corp - expect(result.relations).toHaveLength(1); // Only Alice -> Acme Corp relation + // Both relations included: Alice → Acme Corp (Alice matched) and Bob → Acme Corp (Acme Corp matched) + expect(result.relations).toHaveLength(2); + }); + + it('should include outgoing relations to unmatched entities', async () => { + const result = await manager.searchNodes('Alice'); + expect(result.entities).toHaveLength(1); + // Alice → Acme Corp relation included because Alice is the source + expect(result.relations).toHaveLength(1); + expect(result.relations[0].from).toBe('Alice'); + expect(result.relations[0].to).toBe('Acme Corp'); }); it('should return empty graph for no matches', async () => { @@ -336,16 +346,41 @@ describe('KnowledgeGraphManager', () => { expect(result.entities.map(e => e.name)).toContain('Bob'); }); - it('should include relations between opened nodes', async () => { + it('should include all relations connected to opened nodes', async () => { const result = await manager.openNodes(['Alice', 'Bob']); + // Alice → Bob (both endpoints opened) and Bob → Charlie (Bob is opened) + expect(result.relations).toHaveLength(2); + expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true); + expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true); + }); + + it('should include relations connected to opened nodes', async () => { + const result = await manager.openNodes(['Bob']); + // Bob has two relations: Alice → Bob and Bob → Charlie + expect(result.relations).toHaveLength(2); + expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true); + expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true); + }); + + it('should include outgoing relations to nodes not in the open set', async () => { + // This is the core bug fix for #3137: open_nodes should return + // relations FROM the opened node, even if the target is not opened + const result = await manager.openNodes(['Alice']); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].name).toBe('Alice'); + // Alice → Bob relation is included because Alice is opened expect(result.relations).toHaveLength(1); expect(result.relations[0].from).toBe('Alice'); expect(result.relations[0].to).toBe('Bob'); }); - it('should exclude relations to unopened nodes', async () => { - const result = await manager.openNodes(['Bob']); - expect(result.relations).toHaveLength(0); + it('should include incoming relations from nodes not in the open set', async () => { + const result = await manager.openNodes(['Charlie']); + expect(result.entities).toHaveLength(1); + // Bob → Charlie relation is included because Charlie is opened + expect(result.relations).toHaveLength(1); + expect(result.relations[0].from).toBe('Bob'); + expect(result.relations[0].to).toBe('Charlie'); }); it('should handle opening non-existent nodes', async () => { diff --git a/src/memory/index.ts b/src/memory/index.ts index 600a7edcc8..b560bf1e53 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -197,9 +197,10 @@ export class KnowledgeGraphManager { // Create a Set of filtered entity names for quick lookup const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - // Filter relations to only include those between filtered entities + // Include relations where at least one endpoint matches the search results. + // This lets callers discover connections to nodes outside the result set. const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to) ); const filteredGraph: KnowledgeGraph = { @@ -219,9 +220,12 @@ export class KnowledgeGraphManager { // Create a Set of filtered entity names for quick lookup const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); - // Filter relations to only include those between filtered entities + // Include relations where at least one endpoint is in the requested set. + // Previously this required BOTH endpoints, which meant relations from a + // requested node to an unrequested node were silently dropped — making it + // impossible to discover a node's connections without reading the full graph. const filteredRelations = graph.relations.filter(r => - filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) + filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to) ); const filteredGraph: KnowledgeGraph = {