# Feedback on MCP support for Oxygen.jl

**URL:** <https://discourse.julialang.org/t/feedback-on-mcp-support-for-oxygen-jl/139869>\
**Category:** Web Stack\
**Tags:** web, oxygenjl, ai, mcp\
**Created:** [October 7, 2026, 3:29am UTC](https://discourse.julialang.org/t/feedback-on-mcp-support-for-oxygen-jl/139869 "2026-10-07T03:29:20Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![ndortega](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ndortega/32/37018_2.png) [@ndortega](https://discourse.julialang.org/u/ndortega)\
**Post date:** [October 7, 2026, 3:29am UTC](https://discourse.julialang.org/t/feedback-on-mcp-support-for-oxygen-jl/139869/1 "2026-10-07T03:29:20Z")

</div>

Hello Everyone,

Oxygen.jl is almost ready to ship with built-in support for the MCP protocol, and I wanted to preview the new syntax and collect feedback on the API.

This update includes macros & functions to register `tools`, `prompts`, and `resources`. On top of that, it will ship with a built-in swagger-like UI to explore the MCP server.

 ![tool-demo-rounded](https://global.discourse-cdn.com/julialang/original/3X/b/c/bc955577ac54068d30a814c54e622eed2c4fae6d.png)

With this update, you get the same first-class experience when writing tools as you do when writing normal HTTP routes.

- Automatic schema generation (supports deeply nested structs)
- Automatic serialization on inputs & outputs
- A built-in UI to view and play with the resources (tools, prompts, etc.)

Below is an example of a basic “mixed” server where we register normal HTTP routes alongside tools.

```julia
using Oxygen

@get "/" function ()
    text("welcome to our mcp server")
end

# Here's the most basic version of a tool
@tool "Add two integers" Dict(:a => "first number", :b => "second number") function add(a::Int, b::Int)
    return a + b
end

# Doc strings will be used as the tool description if none is provided
"""
Subtract two integers
"""
@tool Dict(:a => "first number", :b => "second number") function subtract(a::Int, b::Int)
    return a - b
end

serve() # exposes a /mcp routes

```

This API provides the most ergonomic way to define a tool in the Julia ecosystem. Packages like ModelContextProtocol.jl take a schema-first approach where you need to manually define the input schema, whereas Oxygen.jl takes a code-first approach by deriving the schema from the code itself.

Below are some examples that demonstrate the features mentioned above:

## Nested Structs

Structs can be deeply nested and

```julia-auto
@kwdef struct Coordinates
    lat::Float64
    lon::Float64
end

@kwdef struct Place
    name::String
    coordinates::Coordinates
    tags::Vector{String} = String[]
end

"""
Look up a saved place
"""
@tool Dict(:place => "extract the location info from a place") function lookup_place(place::Place)
    return Dict(
        "name" => uppercase(place.name), 
        "lat" => place.coordinates.lat, 
        "lon" => place.coordinates.lon, 
        "tags" => place.tags
    )
end

```

## Tool Streaming

You can also stream values from tools with the mcp\_stream() function, which runs your do-block in a producer task and queues each value into a bounded channel. Those values are forwarded to the client as progress notifications on the same SSE response, and the value you return from the do-block is still the final result.

```julia
@tool "Sync files" Dict(:files => "file names") function sync_files(files::Vector{String})
    return mcp_stream() do stream
        for file in files
            sleep(0.4)
            put!(stream, "synced $file") # auto progress 1, 2, 3, ...
        end
        return "Synced $(length(files)) files"
    end
end

```

## Support for string-based enums

Enums in Julia are integer-backed and don’t come with string handling out of the box. In this update, the serialization layer converts string values into enums when a call comes in, and the generated schema advertises the enum names as a string enum with the default stringified. On the way out, enums already serialize back to their names as strings.

```julia-auto
@enum Greeting hello goodbye

"""
Greet the world
"""
@tool Dict(:style => "Greeting style") function greet(style::Greeting=hello)
    return style == hello ? "Hello, world!" : "Goodbye, world!"
end

```

The tool above generates this schema for the tool, which advertises the string enums

```json
 {"type":"string","enum":["hello","goodbye"],"default":"hello","description":"Greeting style"}

```

## Prompts

```julia
@prompt "Plan a trip to a place" function trip_plan(place::String, days::Int=3)
    return "Plan a $days-day trip to $place."
end

# Can also return a multi-message responses
@prompt "Review a saved place" function review_place(place::String, style::String="concise")
    return ["user" => "Write a $style review of $place.",
            "assistant" => "Sure, what should it focus on?",
            "user" => "Its coordinates, tags, and nearby sites."]
end

```

## Resources

You can also add template variables to a resource URI with {name} placeholders, and each one MUST match a parameter on the attached handler. The handler can’t declare any parameters the template doesn’t name.

```julia
# Sample data used by the resource examples below.
const PLACES = Dict(
    "seattle" => Place("Seattle", Coordinates(47.61, -122.33), ["coffee", "rain"]),
    "kyoto" => Place("Kyoto", Coordinates(35.01, 135.77), ["temples", "gardens"]),
)

@resource "oxygen://places/{name}" "Look up a place by name" function place_resource(name::String)
    place = get(PLACES, lowercase(name), nothing)
    if isnothing(place)
        return "Unknown place: $name"
    end
    return Dict(
        "name" => place.name,
        "lat" => place.coordinates.lat,
        "lon" => place.coordinates.lon,
        "tags" => place.tags,
    )
end

```

This isn’t an exhaustive list of all the features included in this update, but I wanted to share enough examples for others to review and provide feedback.

Nothing’s been released yet, so please let me know what you like and don’t like about this api!
