Skip to content
Merged
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
23 changes: 20 additions & 3 deletions src/everything/resources/session.ts
Original file line number Diff line number Diff line change
@@ -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<string, RegisteredResource>();

/**
* Generates a session-scoped resource URI string based on the provided resource name.
*
Expand Down Expand Up @@ -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 };
};
2 changes: 1 addition & 1 deletion src/fetch/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/fetch/src/mcp_server_fetch/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/fetch/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
47 changes: 41 additions & 6 deletions src/memory/__tests__/knowledge-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
12 changes: 8 additions & 4 deletions src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down
Loading