Skip to content
Draft
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
46 changes: 46 additions & 0 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ local shared_state = {
relay_url = nil,
poll_task = nil,
relay_poll_task = nil,
resource_check_task = nil, -- scheduled resource change check function
last_interaction_time = nil, -- timestamp of last MCP interaction
idle_check_task = nil, -- scheduled idle check function
idle_warning_widget = nil, -- reference to the warning notification widget
Expand Down Expand Up @@ -506,9 +507,15 @@ function MCP:startLocalServer(silent)
shared_state.local_http_running = true
shared_state.last_interaction_time = os.time()

-- Set transport for bidirectional communication
shared_state.protocol:setTransport(shared_state.server)

-- Start polling for requests
self:schedulePoll()

-- Start resource change monitoring
self:scheduleResourceChangeCheck()

-- Start idle timeout checking
self:scheduleIdleCheck()

Expand Down Expand Up @@ -563,6 +570,12 @@ function MCP:stopServer()
-- Mark MCP server as stopped
shared_state.server_running = false

-- Stop resource change checking
if shared_state.resource_check_task then
UIManager:unschedule(shared_state.resource_check_task)
shared_state.resource_check_task = nil
end

-- Stop idle checking
self:cancelIdleCheck()
self:cancelIdleWarning()
Expand Down Expand Up @@ -594,6 +607,33 @@ function MCP:schedulePoll()
UIManager:scheduleIn(0.05, shared_state.poll_task)
end

-- Schedule periodic checks for resource changes
function MCP:scheduleResourceChangeCheck()
if not shared_state.server_running then
return
end

shared_state.resource_check_task = function()
-- Check running state to avoid race conditions during teardown
if not shared_state.server_running then
return
end

-- Check for resource changes and send notifications
if shared_state.protocol then
shared_state.protocol:checkAndNotifyResourceChanges()
end

-- Schedule next check (check every 0.5 seconds for reasonable responsiveness)
-- This interval balances battery life with timely notifications on e-reader devices
if shared_state.server_running then
UIManager:scheduleIn(0.5, shared_state.resource_check_task)
end
end

UIManager:scheduleIn(0.5, shared_state.resource_check_task)
end

-- Cloud Relay management
-- @param show_notification boolean: if true, show startup notification
function MCP:startRelay(show_notification)
Expand All @@ -617,6 +657,12 @@ function MCP:startRelay(show_notification)
shared_state.relay_running = true
shared_state.last_interaction_time = os.time()

-- Set transport for bidirectional communication
shared_state.protocol:setTransport(shared_state.relay)

-- Start resource change monitoring
self:scheduleResourceChangeCheck()

-- Start idle timeout checking
self:scheduleIdleCheck()

Expand Down
37 changes: 37 additions & 0 deletions mcp_protocol.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ local MCPProtocol = {
resources = nil,
tools = nil,
initialized = false,
transport = nil, -- Reference to MCPServer or MCPRelay for bidirectional communication
}

function MCPProtocol:new(o)
Expand All @@ -26,6 +27,10 @@ function MCPProtocol:new(o)
return o
end

function MCPProtocol:setTransport(transport)
self.transport = transport
end

function MCPProtocol:setResources(resources)
self.resources = resources
self.capabilities.resources = {
Expand Down Expand Up @@ -306,4 +311,36 @@ function MCPProtocol:createNotificationResponse()
}
end

-- Send a notification about resource changes
function MCPProtocol:notifyResourcesUpdated(uris)
if not self.transport or not self.initialized then
logger.dbg("MCP Protocol: Cannot send notification - no transport or not initialized")
return false
end

local notification = {
jsonrpc = "2.0",
method = "notifications/resources/updated",
params = {
uris = uris
}
}

logger.dbg("MCP Protocol: Sending resources updated notification for", #uris, "URIs")
return self.transport:sendNotification(notification)
end

-- Check for resource changes and send notifications if needed
function MCPProtocol:checkAndNotifyResourceChanges()
if not self.resources then
return
end

local changed = self.resources:checkForChanges()
if #changed > 0 then
logger.dbg("MCP Protocol: Detected", #changed, "changed resources")
self:notifyResourcesUpdated(changed)
end
end

return MCPProtocol
101 changes: 101 additions & 0 deletions mcp_relay.lua
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,10 @@ function MCPRelay:handlePollResponse(response_body)
-- Handle the request
if request_data.type == "request" and request_data.requestId then
self:handleRelayedRequest(request_data)
elseif request_data.type == "server_response" and request_data.requestId then
-- Response to a server-initiated request
self:handleServerResponse(request_data)
self:scheduleNextPoll()
elseif request_data.type == "ping" then
-- Ping from server means connection is healthy, but no real request
-- Don't count as "empty" since it's just a keep-alive
Expand All @@ -584,6 +588,29 @@ function MCPRelay:handlePollResponse(response_body)
end
end

-- Handle a response to a server-initiated request
function MCPRelay:handleServerResponse(response_data)
local request_id = tostring(response_data.requestId)
logger.dbg("MCP Relay: Received server response for request", request_id)

self._pending_server_requests = self._pending_server_requests or {}
local callback = self._pending_server_requests[request_id]

if callback then
self._pending_server_requests[request_id] = nil

-- Parse the response body
local ok, response = pcall(json.decode, response_data.body or "{}")
if ok then
callback(response)
else
callback(nil, "Failed to parse response")
end
else
logger.warn("MCP Relay: No callback for server response", request_id)
end
end

-- Handle a request relayed from the cloud
function MCPRelay:handleRelayedRequest(request_data)
local request_start_time = socket.gettime()
Expand Down Expand Up @@ -687,6 +714,80 @@ function MCPRelay:sendPong()
end)
end

-- Send a notification to the client (server-initiated message)
-- notification: a JSON-RPC 2.0 notification object
function MCPRelay:sendNotification(notification)
if not self.running or not self.connected then
logger.dbg("MCP Relay: Cannot send notification - not connected")
return false
end

local notify_url = self.relay_url .. "/" .. self.device_id .. "/notify"

local payload = json.encode({
type = "notification",
body = json.encode(notification),
})

logger.dbg("MCP Relay: Sending notification:", notification.method)

self:httpPost(notify_url, payload, function(resp)
if not resp or resp.code ~= 200 then
logger.warn("MCP Relay: Failed to send notification:", resp and resp.code)
else
logger.dbg("MCP Relay: Notification sent successfully")
end
end)

return true
end

-- Send a request to the client (server-initiated, expects response)
-- request: a JSON-RPC 2.0 request object with id
-- callback: function(response) called when client responds
function MCPRelay:sendRequest(request, callback)
if not self.running or not self.connected then
logger.dbg("MCP Relay: Cannot send request - not connected")
if callback then
callback(nil, "Not connected")
end
return false
end

local req_url = self.relay_url .. "/" .. self.device_id .. "/request"

local payload = json.encode({
type = "server_request",
requestId = tostring(request.id),
body = json.encode(request),
})

logger.dbg("MCP Relay: Sending server request:", request.method, "id:", request.id)

-- Store callback for when we get response
self._pending_server_requests = self._pending_server_requests or {}
if callback then
self._pending_server_requests[tostring(request.id)] = callback
end

self:httpPost(req_url, payload, function(resp)
if not resp or resp.code ~= 200 then
logger.warn("MCP Relay: Failed to send server request:", resp and resp.code)
-- Call callback with error
local cb = self._pending_server_requests[tostring(request.id)]
if cb then
self._pending_server_requests[tostring(request.id)] = nil
cb(nil, "Failed to send request")
end
else
logger.dbg("MCP Relay: Server request sent, awaiting response")
-- Response will come through the poll mechanism
end
end)

return true
end

-- Handle disconnection
function MCPRelay:handleDisconnect()
-- Don't trigger reconnect if already reconnecting or registering
Expand Down
Loading