Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/actions/action_default_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "action_jsonschema_serve_v1.h"
#include "action_serve_metapack_file_v1.h"
#include "mcp.h"

#include <filesystem> // std::filesystem
#include <span> // std::span
Expand Down Expand Up @@ -110,6 +111,11 @@ class ActionDefault_v1 : public sourcemeta::one::Action {
}
}

auto mcp(const sourcemeta::core::JSON &) -> sourcemeta::core::JSON override {
return sourcemeta::one::mcp_error("mcp-not-supported",
"This action cannot be invoked via MCP");
}

private:
std::string_view error_schema_;
};
Expand Down
6 changes: 6 additions & 0 deletions src/actions/action_health_check_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <sourcemeta/one/actions.h>
#include <sourcemeta/one/http.h>

#include "mcp.h"

#include <filesystem> // std::filesystem
#include <span> // std::span
#include <string> // std::string
Expand Down Expand Up @@ -42,6 +44,10 @@ class ActionHealthCheck_v1 : public sourcemeta::one::Action {
response);
}

auto mcp(const sourcemeta::core::JSON &) -> sourcemeta::core::JSON override {
return sourcemeta::one::mcp_empty();
}

private:
std::string_view error_schema_;
};
Expand Down
48 changes: 44 additions & 4 deletions src/actions/action_jsonschema_evaluate_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <sourcemeta/one/metapack.h>
#include <sourcemeta/one/shared.h>

#include "mcp.h"

#include <cassert> // assert
#include <cstdint> // std::uint8_t
#include <filesystem> // std::filesystem::path
Expand Down Expand Up @@ -57,10 +59,7 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::Action {
sourcemeta::one::send_response(sourcemeta::one::STATUS_NO_CONTENT,
request, response);
} else if (request.method() == "post") {
auto template_path{this->base() / "schemas"};
template_path /= path;
template_path /= "%";
template_path /= "blaze-exhaustive.metapack";
auto template_path{this->build_template_path(path)};
if (!std::filesystem::exists(template_path)) {
const auto schema_path{template_path.parent_path() / "schema.metapack"};
if (std::filesystem::exists(schema_path)) {
Expand Down Expand Up @@ -106,9 +105,50 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::Action {
}
}

auto mcp(const sourcemeta::core::JSON &input)
-> sourcemeta::core::JSON override {
assert(input.is_object());
assert(input.defines("schema"));
assert(input.at("schema").is_string());
assert(input.defines("instance"));

const auto template_path{
this->build_template_path(input.at("schema").to_string())};
if (!std::filesystem::exists(template_path)) {
const auto schema_path{template_path.parent_path() / "schema.metapack"};
if (std::filesystem::exists(schema_path)) {
return sourcemeta::one::mcp_error(
"no-template",
"This schema was not precompiled for schema evaluation");
}
return sourcemeta::one::mcp_error("schema-not-found",
"There is no schema at that path");
}

std::ostringstream stringified_instance;
sourcemeta::core::stringify(input.at("instance"), stringified_instance);

try {
return sourcemeta::one::mcp_json(this->evaluate(
template_path, stringified_instance.str(), this->mode_));
} catch (const std::exception &exception) {
return sourcemeta::one::mcp_error("evaluation-error", exception.what());
}
}

enum class EvaluateMode : std::uint8_t { Standard, Trace };

private:
[[nodiscard]] auto
build_template_path(const std::string_view schema_path) const
-> std::filesystem::path {
auto template_path{this->base() / "schemas"};
template_path /= schema_path;
template_path /= "%";
template_path /= "blaze-exhaustive.metapack";
return template_path;
}

auto
resolve_vocabulary(const std::string_view keyword_location,
const sourcemeta::core::WeakPointer &evaluate_path,
Expand Down
6 changes: 6 additions & 0 deletions src/actions/action_jsonschema_serve_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <sourcemeta/one/shared.h>

#include "action_serve_metapack_file_v1.h"
#include "mcp.h"

#include <algorithm> // std::ranges::transform
#include <cctype> // std::tolower
Expand Down Expand Up @@ -85,6 +86,11 @@ class ActionJSONSchemaServe_v1 : public sourcemeta::one::Action {
this->error_schema_);
}

auto mcp(const sourcemeta::core::JSON &) -> sourcemeta::core::JSON override {
return sourcemeta::one::mcp_error("mcp-not-supported",
"This action cannot be invoked via MCP");
}

private:
std::string_view error_schema_;
};
Expand Down
7 changes: 7 additions & 0 deletions src/actions/action_not_found_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <sourcemeta/one/actions.h>
#include <sourcemeta/one/http.h>

#include "mcp.h"

#include <filesystem> // std::filesystem
#include <span> // std::span
#include <string> // std::string
Expand Down Expand Up @@ -33,6 +35,11 @@ class ActionNotFound_v1 : public sourcemeta::one::Action {
"There is nothing at this URL", this->error_schema_);
}

auto mcp(const sourcemeta::core::JSON &) -> sourcemeta::core::JSON override {
return sourcemeta::one::mcp_error("mcp-not-supported",
"This action cannot be invoked via MCP");
}

private:
std::string_view error_schema_;
};
Expand Down
154 changes: 98 additions & 56 deletions src/actions/action_schema_search_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
#include <sourcemeta/one/http.h>
#include <sourcemeta/one/search.h>

#include "mcp.h"

#include <cassert> // assert
#include <charconv> // std::from_chars
#include <cstdint> // std::uint8_t
#include <cstdint> // std::uint8_t, std::int64_t
#include <filesystem> // std::filesystem
#include <optional> // std::optional, std::nullopt
#include <span> // std::span
#include <sstream> // std::ostringstream
#include <string> // std::string
#include <string_view> // std::string_view
#include <system_error> // std::errc
#include <utility> // std::cmp_less_equal

class ActionSchemaSearch_v1 : public sourcemeta::one::Action {
public:
Expand Down Expand Up @@ -55,7 +60,6 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::Action {
return;
}

constexpr std::size_t MAXIMUM_QUERY_LENGTH{256};
if (query.size() > MAXIMUM_QUERY_LENGTH) {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_BAD_REQUEST,
Expand All @@ -65,17 +69,15 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::Action {
return;
}

constexpr std::size_t DEFAULT_LIMIT{10};
constexpr std::size_t MAXIMUM_LIMIT{100};
std::size_t limit{DEFAULT_LIMIT};
const auto limit_param{request.query("limit")};
if (!limit_param.empty()) {
const auto limit_parameter{request.query("limit")};
if (!limit_parameter.empty()) {
std::size_t parsed_limit{0};
const auto [pointer, error_code] = std::from_chars(
limit_param.data(), limit_param.data() + limit_param.size(),
parsed_limit);
limit_parameter.data(),
limit_parameter.data() + limit_parameter.size(), parsed_limit);
if (error_code != std::errc{} ||
pointer != limit_param.data() + limit_param.size() ||
pointer != limit_parameter.data() + limit_parameter.size() ||
parsed_limit < 1 || parsed_limit > MAXIMUM_LIMIT) {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_BAD_REQUEST,
Expand All @@ -88,55 +90,18 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::Action {
limit = parsed_limit;
}

std::uint8_t scope{sourcemeta::one::SearchScopePath |
sourcemeta::one::SearchScopeTitle |
sourcemeta::one::SearchScopeDescription};
const auto scope_param{request.query("scope")};
if (!scope_param.empty()) {
scope = 0;
std::string_view remaining{scope_param};
while (!remaining.empty()) {
const auto comma{remaining.find(',')};
const auto token{comma != std::string_view::npos
? remaining.substr(0, comma)
: remaining};
if (token.empty()) {
// Skip empty tokens from trailing commas
} else if (token == "path") {
scope |= sourcemeta::one::SearchScopePath;
} else if (token == "title") {
scope |= sourcemeta::one::SearchScopeTitle;
} else if (token == "description") {
scope |= sourcemeta::one::SearchScopeDescription;
} else {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_BAD_REQUEST,
"invalid-search-scope",
"The scope must be a comma-separated list of: path, title, "
"description",
this->error_schema_);
return;
}

if (comma != std::string_view::npos) {
remaining = remaining.substr(comma + 1);
} else {
break;
}
}

if (scope == 0) {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_BAD_REQUEST,
"invalid-search-scope",
"The scope must be a comma-separated list of: path, title, "
"description",
this->error_schema_);
return;
}
const auto parsed_scope{parse_scope(request.query("scope"))};
if (!parsed_scope.has_value()) {
sourcemeta::one::json_error(
request, response, sourcemeta::one::STATUS_BAD_REQUEST,
"invalid-search-scope",
"The scope must be a comma-separated list of: path, title, "
"description",
this->error_schema_);
return;
}

auto result{this->search_view_.search(query, limit, scope)};
auto result{this->search_view_.search(query, limit, parsed_scope.value())};
response.write_status(sourcemeta::one::STATUS_OK);
response.write_header("Access-Control-Allow-Origin", "*");
response.write_header("Content-Type", "application/json");
Expand All @@ -148,7 +113,84 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::Action {
sourcemeta::one::Encoding::Identity);
}

auto mcp(const sourcemeta::core::JSON &input)
-> sourcemeta::core::JSON override {
assert(input.is_object());
assert(input.defines("q"));
assert(input.at("q").is_string());
const auto &query{input.at("q").to_string()};
assert(!query.empty());
assert(query.size() <= MAXIMUM_QUERY_LENGTH);

std::size_t limit{DEFAULT_LIMIT};
if (input.defines("limit")) {
assert(input.at("limit").is_integer());
const auto parsed_limit{input.at("limit").to_integer()};
assert(parsed_limit >= 1);
assert(std::cmp_less_equal(parsed_limit, MAXIMUM_LIMIT));
limit = static_cast<std::size_t>(parsed_limit);
}

std::string_view scope_parameter;
if (input.defines("scope")) {
assert(input.at("scope").is_string());
scope_parameter = input.at("scope").to_string();
}

const auto parsed_scope{parse_scope(scope_parameter)};
assert(parsed_scope.has_value());

return sourcemeta::one::mcp_json(
this->search_view_.search(query, limit, parsed_scope.value()));
}

private:
static auto parse_scope(const std::string_view scope_parameter)
-> std::optional<std::uint8_t> {
std::uint8_t scope{sourcemeta::one::SearchScopePath |
sourcemeta::one::SearchScopeTitle |
sourcemeta::one::SearchScopeDescription};
if (scope_parameter.empty()) {
return scope;
}

scope = 0;
std::string_view remaining{scope_parameter};
while (!remaining.empty()) {
const auto comma{remaining.find(',')};
const auto token{comma != std::string_view::npos
? remaining.substr(0, comma)
: remaining};
if (token.empty()) {
// Skip empty tokens from trailing commas
} else if (token == "path") {
scope |= sourcemeta::one::SearchScopePath;
} else if (token == "title") {
scope |= sourcemeta::one::SearchScopeTitle;
} else if (token == "description") {
scope |= sourcemeta::one::SearchScopeDescription;
} else {
return std::nullopt;
}

if (comma != std::string_view::npos) {
remaining = remaining.substr(comma + 1);
} else {
break;
}
}

if (scope == 0) {
return std::nullopt;
}

return scope;
}

static constexpr std::size_t MAXIMUM_QUERY_LENGTH{256};
static constexpr std::size_t DEFAULT_LIMIT{10};
static constexpr std::size_t MAXIMUM_LIMIT{100};

sourcemeta::one::SearchView search_view_;
std::string_view response_schema_;
std::string_view error_schema_;
Expand Down
23 changes: 23 additions & 0 deletions src/actions/action_serve_explorer_artifact_v1.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

#include <sourcemeta/one/actions.h>
#include <sourcemeta/one/http.h>
#include <sourcemeta/one/metapack.h>

#include "action_serve_metapack_file_v1.h"
#include "mcp.h"

#include <cassert> // assert
#include <filesystem> // std::filesystem
#include <span> // std::span
#include <string> // std::string
Expand Down Expand Up @@ -45,6 +48,26 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::Action {
response, this->error_schema_);
}

auto mcp(const sourcemeta::core::JSON &input)
-> sourcemeta::core::JSON override {
assert(input.is_object());

auto absolute_path{this->base() / "explorer"};
if (input.defines("path")) {
assert(input.at("path").is_string());
absolute_path /= input.at("path").to_string();
}
absolute_path /= "%";
absolute_path /= std::string{this->artifact_} + ".metapack";

auto payload{sourcemeta::one::metapack_read_json(absolute_path)};
if (!payload.has_value()) {
return sourcemeta::one::mcp_error("not-found",
"There is nothing at this URL");
}
return sourcemeta::one::mcp_json(std::move(payload).value());
}

private:
std::string_view artifact_;
std::string_view response_schema_;
Expand Down
Loading
Loading