Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

Dispatch.pudu

Pudu86 lines3.4 KB

GitHub ↗
1/** @App.Dispatch.Backbone — answers one input line under the session */2module PuduLangMcp.App.Dispatch34import Std.Json as Json5import Std.Map as Map6import Std.Text as Text7import PuduLangMcp.App.Completion as Completion8import PuduLangMcp.App.Context as Context9import PuduLangMcp.App.Discovery as Discovery10import PuduLangMcp.App.Prompts as Prompts11import PuduLangMcp.App.Resources as Resources12import PuduLangMcp.App.Tools.Registry as Registry13import PuduLangMcp.Constants.Server as Server14import PuduLangMcp.Domain.Protocol.Negotiation as Negotiation15import PuduLangMcp.Domain.Rpc.Message as Message16import PuduLangMcp.Domain.Rpc.Reply as Reply17import PuduLangMcp.Errors.RpcError as RpcError1819/** @App.Dispatch.State — the session carried between input lines */20export type State = { session: Negotiation.Session }2122/// The method a legacy client checks the connection with.23const PING: Str = "ping"2425/// The state before any input.26export fn initial() -> State {27  State{session: Negotiation.Unopened}28}2930/// The handler of every stateless method, by method name.31export fn routes() -> Map[Str, fn(&Context.Context, &Json.Json) -> Result[Json.Json, RpcError.ProtocolError]] {32  mapOf([33      ("server/discover", Discovery.discover),34      ("tools/list", Registry.list),35      ("tools/call", Registry.call),36      ("resources/list", Resources.list),37      ("resources/templates/list", Resources.templates),38      ("resources/read", Resources.read),39      ("prompts/list", Prompts.list),40      ("prompts/get", Prompts.get),41      ("completion/complete", Completion.complete)42    ])43}4445/// The next state and every reply line for one input line.46export fn respond(context: &Context.Context, state: &State, line: Str) -> (State, Array[Str]) {47  if line.toBytes().length() > Server.MAX_LINE_BYTES {48    return (*state, [Reply.failure(&None, &RpcError.InvalidRequest("a message may be at most " + show(Server.MAX_LINE_BYTES) + " bytes"))])49  }50  if Text.isBlank(line) { return (*state, []) }51  match Message.parse(line) {52    case Message.Refused(id, problem) => (*state, [Reply.failure(&id, &problem)])53    case Message.ClientResponse => (*state, [])54    case Message.Notification(_, _) => (*state, [])55    case Message.Request(id, method, params) => request(context, state, id, method, &params)56  }57}5859/// The next state and the reply for one admitted or refused request.60fn request(context: &Context.Context, state: &State, id: Message.RequestId, method: Str, params: &Json.Json) -> (State, Array[Str]) {61  let era = match Negotiation.admit(&state.session, method, params) {62    case Ok(admitted) => admitted63    case Err(problem) => { return (*state, [Reply.failure(&Some(id), &problem)]) }64  }65  let unknown = Reply.failure(&Some(id), &RpcError.MethodNotFound(method))66  if Negotiation.isHandshake(method) {67    return match era {68      case Negotiation.LegacyEra(version) => (State{session: Negotiation.Legacy(version)}, [Reply.result(&id, &Discovery.initialize(version))])69      case Negotiation.Modern => (*state, [unknown])70    }71  }72  if method == PING {73    return match era {74      case Negotiation.LegacyEra(_) => (*state, [Reply.result(&id, &Discovery.pong())])75      case Negotiation.Modern => (*state, [unknown])76    }77  }78  match Map.get(&routes(), method) {79    case Some(handler) => match handler(context, params) {80      case Ok(body) => (*state, [Reply.result(&id, &body)])81      case Err(problem) => (*state, [Reply.failure(&Some(id), &problem)])82    }83    case None => (*state, [unknown])84  }85}86