feat: add /.well-known discovery endpoints for agent readiness
Adds WellKnownController with 6 public, no-auth endpoints: api-catalog, openid-configuration, oauth-authorization-server, oauth-protected-resource, mcp/server-card.json, and agent-skills/index.json.
This commit is contained in:
parent
20df4df566
commit
0bb76dbe68
3 changed files with 167 additions and 0 deletions
84
lib/towerops_web/controllers/well_known_controller.ex
Normal file
84
lib/towerops_web/controllers/well_known_controller.ex
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
defmodule ToweropsWeb.WellKnownController do
|
||||
@moduledoc false
|
||||
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
def api_catalog(conn, _params) do
|
||||
base_url = ToweropsWeb.Endpoint.url()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/linkset+json")
|
||||
|> json(%{
|
||||
"linkset" => [
|
||||
%{
|
||||
"anchor" => "#{base_url}/api/v1",
|
||||
"service-doc" => [%{"href" => "#{base_url}/docs/api"}],
|
||||
"service-desc" => [%{"href" => "#{base_url}/docs/api", "type" => "text/html"}],
|
||||
"status" => [%{"href" => "#{base_url}/health"}]
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
def openid_configuration(conn, _params) do
|
||||
base_url = ToweropsWeb.Endpoint.url()
|
||||
|
||||
json(conn, %{
|
||||
"issuer" => base_url,
|
||||
"authorization_endpoint" => "#{base_url}/oauth/authorize",
|
||||
"token_endpoint" => "#{base_url}/oauth/token",
|
||||
"jwks_uri" => "#{base_url}/.well-known/jwks.json",
|
||||
"grant_types_supported" => ["authorization_code"],
|
||||
"response_types_supported" => ["code"]
|
||||
})
|
||||
end
|
||||
|
||||
def oauth_authorization_server(conn, _params) do
|
||||
base_url = ToweropsWeb.Endpoint.url()
|
||||
|
||||
json(conn, %{
|
||||
"issuer" => base_url,
|
||||
"authorization_endpoint" => "#{base_url}/oauth/authorize",
|
||||
"token_endpoint" => "#{base_url}/oauth/token",
|
||||
"jwks_uri" => "#{base_url}/.well-known/jwks.json",
|
||||
"grant_types_supported" => ["authorization_code"],
|
||||
"response_types_supported" => ["code"]
|
||||
})
|
||||
end
|
||||
|
||||
def oauth_protected_resource(conn, _params) do
|
||||
base_url = ToweropsWeb.Endpoint.url()
|
||||
|
||||
json(conn, %{
|
||||
"resource" => "#{base_url}/api/v1",
|
||||
"authorization_servers" => [base_url],
|
||||
"scopes_supported" => ["read", "write"]
|
||||
})
|
||||
end
|
||||
|
||||
def mcp_server_card(conn, _params) do
|
||||
base_url = ToweropsWeb.Endpoint.url()
|
||||
|
||||
json(conn, %{
|
||||
"serverInfo" => %{
|
||||
"name" => "TowerOps",
|
||||
"version" => "1.0.0"
|
||||
},
|
||||
"transport" => %{
|
||||
"endpoint" => "#{base_url}/mcp"
|
||||
},
|
||||
"capabilities" => %{
|
||||
"tools" => %{},
|
||||
"resources" => %{},
|
||||
"prompts" => %{}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def agent_skills(conn, _params) do
|
||||
json(conn, %{
|
||||
"$schema" => "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
||||
"skills" => []
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
@ -85,6 +85,19 @@ defmodule ToweropsWeb.Router do
|
|||
get "/sitemap.xml", SitemapController, :index
|
||||
end
|
||||
|
||||
# Well-known discovery endpoints (no pipeline, public access, no auth/CSRF)
|
||||
scope "/.well-known", ToweropsWeb do
|
||||
get "/api-catalog", WellKnownController, :api_catalog
|
||||
get "/openid-configuration", WellKnownController, :openid_configuration
|
||||
get "/oauth-authorization-server", WellKnownController, :oauth_authorization_server
|
||||
get "/oauth-protected-resource", WellKnownController, :oauth_protected_resource
|
||||
get "/agent-skills/index.json", WellKnownController, :agent_skills
|
||||
end
|
||||
|
||||
scope "/.well-known/mcp", ToweropsWeb do
|
||||
get "/server-card.json", WellKnownController, :mcp_server_card
|
||||
end
|
||||
|
||||
scope "/", ToweropsWeb do
|
||||
pipe_through :browser
|
||||
|
||||
|
|
|
|||
70
test/towerops_web/controllers/well_known_controller_test.exs
Normal file
70
test/towerops_web/controllers/well_known_controller_test.exs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
defmodule ToweropsWeb.WellKnownControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
describe "GET /.well-known/api-catalog" do
|
||||
test "returns linkset+json with API entries", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/api-catalog")
|
||||
assert get_resp_header(conn, "content-type") == ["application/linkset+json; charset=utf-8"]
|
||||
body = json_response(conn, 200)
|
||||
assert is_list(body["linkset"])
|
||||
[entry] = body["linkset"]
|
||||
assert is_binary(entry["anchor"])
|
||||
assert is_list(entry["service-doc"])
|
||||
assert is_list(entry["service-desc"])
|
||||
assert is_list(entry["status"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /.well-known/openid-configuration" do
|
||||
test "returns OIDC discovery metadata", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/openid-configuration")
|
||||
body = json_response(conn, 200)
|
||||
assert is_binary(body["issuer"])
|
||||
assert is_binary(body["authorization_endpoint"])
|
||||
assert is_binary(body["token_endpoint"])
|
||||
assert is_binary(body["jwks_uri"])
|
||||
assert is_list(body["grant_types_supported"])
|
||||
assert is_list(body["response_types_supported"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /.well-known/oauth-authorization-server" do
|
||||
test "returns OAuth 2.0 server metadata", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/oauth-authorization-server")
|
||||
body = json_response(conn, 200)
|
||||
assert is_binary(body["issuer"])
|
||||
assert is_binary(body["authorization_endpoint"])
|
||||
assert is_binary(body["token_endpoint"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /.well-known/oauth-protected-resource" do
|
||||
test "returns protected resource metadata", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/oauth-protected-resource")
|
||||
body = json_response(conn, 200)
|
||||
assert is_binary(body["resource"])
|
||||
assert is_list(body["authorization_servers"])
|
||||
assert is_list(body["scopes_supported"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /.well-known/mcp/server-card.json" do
|
||||
test "returns MCP server card", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/mcp/server-card.json")
|
||||
body = json_response(conn, 200)
|
||||
assert get_in(body, ["serverInfo", "name"]) == "TowerOps"
|
||||
assert is_binary(get_in(body, ["serverInfo", "version"]))
|
||||
assert is_binary(get_in(body, ["transport", "endpoint"]))
|
||||
assert is_map(body["capabilities"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /.well-known/agent-skills/index.json" do
|
||||
test "returns agent skills discovery index", %{conn: conn} do
|
||||
conn = get(conn, "/.well-known/agent-skills/index.json")
|
||||
body = json_response(conn, 200)
|
||||
assert body["$schema"] =~ "agentskills.io"
|
||||
assert is_list(body["skills"])
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue