← Files FievelARCHIVED FILE
runtime/scripts/mcp_protocol.py
5 KB · Nov 6, 2026 · 00:15 UTC
"""Minimal Model Context Protocol (MCP) message handling shared by Fievel's servers.
Implements the JSON-RPC 2.0 methods a tools-only server needs: initialize, ping,
tools/list and tools/call. No dependencies, no network access and no model calls.
"""
import json
SUPPORTED_VERSIONS = ('2025-06-18', '2025-03-26', '2024-11-05')
PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR = -32700, -32600, -32601, -32602, -32603
class Tool:
"""A named tool with a JSON Schema for its arguments and a handler returning JSON data."""
def __init__(self, name, title, description, properties, required, handler, read_only=True):
self.name, self.handler = name, handler
self.definition = {
'name': name, 'title': title, 'description': description,
'inputSchema': {'type': 'object', 'properties': properties,
'required': list(required), 'additionalProperties': False},
'annotations': {'title': title, 'readOnlyHint': read_only, 'destructiveHint': False,
'idempotentHint': read_only, 'openWorldHint': False},
}
def _error(request_id, code, message):
return {'jsonrpc': '2.0', 'id': request_id, 'error': {'code': code, 'message': message}}
def _check_arguments(tool, arguments):
schema = tool.definition['inputSchema']
if not isinstance(arguments, dict):
raise ValueError('Tool arguments must be an object.')
unknown = set(arguments) - set(schema['properties'])
missing = [name for name in schema['required'] if name not in arguments]
if unknown:
raise ValueError('Unknown argument(s): ' + ', '.join(sorted(unknown)))
if missing:
raise ValueError('Missing argument(s): ' + ', '.join(missing))
kinds = {'string': str, 'boolean': bool, 'object': dict, 'integer': int}
for name, value in arguments.items():
expected = kinds.get(schema['properties'][name].get('type'))
if expected and (not isinstance(value, expected) or (expected is int and isinstance(value, bool))):
raise ValueError(f'Argument {name} must be of type {schema["properties"][name]["type"]}.')
class Server:
def __init__(self, name, version, instructions, tools):
self.info = {'name': name, 'title': 'Fievel', 'version': version}
self.instructions = instructions
self.tools = {tool.name: tool for tool in tools}
def handle(self, message):
"""Return the JSON-RPC response for one message, or None for a notification."""
if not isinstance(message, dict) or message.get('jsonrpc') != '2.0' or not isinstance(message.get('method'), str):
return _error(message.get('id') if isinstance(message, dict) else None, INVALID_REQUEST, 'Invalid JSON-RPC request.')
if 'id' not in message:
return None # Notifications such as notifications/initialized need no reply.
request_id, method, params = message['id'], message['method'], message.get('params') or {}
if not isinstance(params, dict):
return _error(request_id, INVALID_PARAMS, 'params must be an object.')
if method == 'initialize':
requested = params.get('protocolVersion')
version = requested if requested in SUPPORTED_VERSIONS else SUPPORTED_VERSIONS[0]
return {'jsonrpc': '2.0', 'id': request_id, 'result': {
'protocolVersion': version, 'capabilities': {'tools': {'listChanged': False}},
'serverInfo': self.info, 'instructions': self.instructions}}
if method == 'ping':
return {'jsonrpc': '2.0', 'id': request_id, 'result': {}}
if method == 'tools/list':
return {'jsonrpc': '2.0', 'id': request_id,
'result': {'tools': [tool.definition for tool in self.tools.values()]}}
if method == 'tools/call':
tool = self.tools.get(params.get('name'))
if tool is None:
return _error(request_id, INVALID_PARAMS, f'Unknown tool: {params.get("name")}')
arguments = params.get('arguments', {})
try:
_check_arguments(tool, arguments)
data = tool.handler(**arguments)
except (ValueError, FileExistsError, FileNotFoundError, NotADirectoryError, IsADirectoryError, PermissionError) as exc:
# Tool failures are results the agent can read and act on, not protocol errors.
return {'jsonrpc': '2.0', 'id': request_id, 'result': {
'content': [{'type': 'text', 'text': f'Error: {exc}'}], 'isError': True}}
except Exception as exc: # A server must answer every request.
return _error(request_id, INTERNAL_ERROR, f'{type(exc).__name__}: {exc}')
return {'jsonrpc': '2.0', 'id': request_id, 'result': {
'content': [{'type': 'text', 'text': json.dumps(data, ensure_ascii=False, indent=2)}],
'structuredContent': data, 'isError': False}}
return _error(request_id, METHOD_NOT_FOUND, f'Unknown method: {method}')
SHA-256: 804e6c01810b8aa28d523cdda6db50249718dc437892517fb9f50035ad007627