///|
/// Who may keep a cached list result: any intermediary, or only caches inside
/// the same authorisation context (the `cacheScope` of a cacheable result).
pub(all) enum Scope {
  Public
  Private
} derive(Eq, Debug)

///|
pub extend Scope with Eq::{equal, not_equal}

///|
pub extend Scope with Debug::{to_repr}

///|
/// An MCP server (← FastMCP's `FastMCP`): the tools, resources, resource
/// templates and prompts it offers, and who it says it is.
///
/// It speaks both eras of the protocol. A request carrying its protocol version
/// in `_meta` is served statelessly under the 2026-07-28 revision; an
/// `initialize` request opens the handshake of the revisions before it. The
/// same server is reached over Streamable HTTP with `to_asgi`, over stdio with
/// `run_stdio`, or by any transport of your own through `handle`.
///
/// `ttl_ms` and `scope` are the caching hints every list result carries under
/// the 2026-07-28 revision. The presets say "stale at once" and "this caller
/// only", the cautious end of both, since a server that changes what it lists
/// per caller would otherwise leak one caller's list to the next.
pub struct Server {
  info : @types.Implementation
  instructions : String?
  ttl_ms : Int32
  scope : Scope
  priv tools : Map[String, @tools.Tool]
  priv checks : Map[String, @moonschema.Schema]
  priv resources : Map[String, @resources.Resource]
  priv templates : Array[@resources.ResourceTemplate]
  priv prompts : Map[String, @prompts.Prompt]
}

///|
pub fn Server::new(
  name : String,
  version~ : String = "0.1.0",
  title? : String,
  instructions? : String,
  ttl_ms~ : Int32 = 0,
  scope~ : Scope = Private,
) -> Server {
  {
    info: { name, title, version, },
    instructions,
    ttl_ms,
    scope,
    tools: Map([]),
    checks: Map([]),
    resources: Map([]),
    templates: [],
    prompts: Map([]),
  }
}

///|
/// Offer `tool`. Its input schema is compiled here, so a call is checked
/// against it before the tool runs. A schema that does not compile is a
/// mistake in the program, not in a request, so it aborts now rather than
/// failing the first call. A second tool under the same name replaces the
/// first.
pub fn Server::add_tool(self : Server, tool : @tools.Tool) -> Unit {
  let name = tool.component.name
  let check = @moonschema.Schema::new(tool.parameters) catch {
    e => abort("tool '\{name}': input schema does not compile: \{e}")
  }
  self.tools[name] = tool
  self.checks[name] = check
}

///|
/// Offer a tool whose handler returns any JSON value (← `@mcp.tool`). An object
/// becomes structured content as well as text; anything else becomes text. An
/// error the handler raises becomes a result marked `isError`, which the model
/// reads and can correct itself from. A name the protocol does not allow aborts
/// at registration.
pub fn Server::tool(
  self : Server,
  name : String,
  run : async (Map[String, Json]) -> Json,
  input_schema~ : Json = { "type": "object" },
  title? : String,
  description? : String,
  output_schema? : Json,
  annotations? : @types.ToolAnnotations,
) -> Unit {
  let shape = @tools.Tool::new(
    name,
    parameters=input_schema,
    run_fn=_ => @tools.ToolResult::new(),
    title?,
    description?,
    output_schema?,
    annotations?,
  ) catch {
    e => abort("tool '\{name}': \{text_of(e)}")
  }
  self.add_tool({
    ..shape,
    run_fn: args => {
      let value = run(args) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        e => raise @exceptions.Tool(text_of(e))
      }
      shape.convert_result(value)
    },
  })
}

///|
/// Offer `resource` at its URI. A second resource at the same URI replaces the
/// first.
pub fn Server::add_resource(
  self : Server,
  resource : @resources.Resource,
) -> Unit {
  self.resources[resource.uri] = resource
}

///|
/// Offer a resource read as text (← `@mcp.resource(uri)`).
pub fn Server::resource(
  self : Server,
  uri : String,
  read : async () -> String,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  self.add_resource(
    @resources.Resource::from_function(
      uri,
      read_fn=() => {
        let text = read() catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
          e => raise @exceptions.Resource(text_of(e))
        }
        Text(text)
      },
      name?,
      title?,
      description?,
      mime_type~,
    ),
  )
}

///|
/// Offer `template`; a URI no fixed resource answers is matched against the
/// templates in the order they were added.
pub fn Server::add_template(
  self : Server,
  template : @resources.ResourceTemplate,
) -> Unit {
  self.templates.push(template)
}

///|
/// Offer the resources an RFC 6570 template names, read as text with the
/// parameters the URI matched (← `@mcp.resource("users://{id}")`). A template
/// that does not parse aborts at registration.
pub fn Server::template(
  self : Server,
  uri_template : String,
  read : async (Map[String, Json]) -> String,
  name? : String,
  title? : String,
  description? : String,
  mime_type~ : String = "text/plain",
) -> Unit {
  let properties : Map[String, Json] = Map([])
  for param in @resources.parse_template_params(uri_template) {
    properties[param] = { "type": "string" }
  }
  let template = @resources.ResourceTemplate::new(
    uri_template,
    parameters={ "type": "object", "properties": Json(properties) },
    read_fn=params => {
      let text = read(params) catch {
        @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
        @exceptions.NotFound(m) => raise @exceptions.NotFound(m)
        e => raise @exceptions.Resource(text_of(e))
      }
      Text(text)
    },
    name?,
    title?,
    description?,
    mime_type~,
  ) catch {
    e => abort("resource template '\{uri_template}': \{text_of(e)}")
  }
  self.add_template(template)
}

///|
/// Offer `prompt`. A second prompt under the same name replaces the first.
pub fn Server::add_prompt(self : Server, prompt : @prompts.Prompt) -> Unit {
  self.prompts[prompt.component.name] = prompt
}

///|
/// Offer a prompt rendered as one user message (← `@mcp.prompt`).
pub fn Server::prompt(
  self : Server,
  name : String,
  render : async (Map[String, Json]) -> String,
  title? : String,
  description? : String,
  arguments~ : Array[@types.PromptArgument] = [],
) -> Unit {
  self.add_prompt(
    @prompts.Prompt::new(
      name,
      render_fn=args => {
        let text = render(args) catch {
          @exceptions.Mcp(data) => raise @exceptions.Mcp(data)
          @exceptions.Validation(m) => raise @exceptions.Validation(m)
          e => raise @exceptions.Prompt(text_of(e))
        }
        Text(text)
      },
      title?,
      description?,
      arguments~,
    ),
  )
}