mcp w.i.p.

This commit is contained in:
Syoyo Fujita
2025-07-24 12:54:35 +09:00
parent 4a8fbab8cb
commit 0ceb63b0dc
13 changed files with 681 additions and 152 deletions

View File

@@ -1,9 +1,18 @@
hostname=localhost
port_no=8085
entrypoint=mcp
sess_id=`cat sess_id.txt | tr -d '\r'`
sess_header="mcp-session-id: ${sess_id}"
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "${sess_header}" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 2
}' \
http://localhost:8085/mcp
http://${hostname}:${port_no}/${entrypoint}

File diff suppressed because one or more lines are too long

View File

@@ -716,5 +716,145 @@ double atof(const std::string &s) {
return atof(s.c_str());
}
/*
base64.cpp and base64.h
Copyright (C) 2004-2008 René Nyffenegger
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wsign-conversion"
#pragma clang diagnostic ignored "-Wconversion"
#endif
static inline bool is_base64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}
std::string base64_encode(unsigned char const *bytes_to_encode,
unsigned int in_len) {
std::string ret;
int i = 0;
int j = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
const char *base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] =
((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] =
((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
for (j = i; j < 3; j++) char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] =
((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] =
((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]];
while ((i++ < 3)) ret += '=';
}
return ret;
}
std::string base64_decode(std::string const &encoded_string) {
int in_len = static_cast<int>(encoded_string.size());
int i = 0;
int j = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string ret;
const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
while (in_len-- && (encoded_string[in_] != '=') &&
is_base64(encoded_string[in_])) {
char_array_4[i++] = encoded_string[in_];
in_++;
if (i == 4) {
for (i = 0; i < 4; i++)
char_array_4[i] =
static_cast<unsigned char>(base64_chars.find(char_array_4[i]));
char_array_3[0] =
(char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] =
((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; (i < 3); i++) ret += char_array_3[i];
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++) char_array_4[j] = 0;
for (j = 0; j < 4; j++)
char_array_4[j] =
static_cast<unsigned char>(base64_chars.find(char_array_4[j]));
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] =
((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
}
return ret;
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
/*
-- end base64.cpp and base64.h
*/
} // namespace tinyusdz

View File

@@ -400,4 +400,8 @@ inline std::string join(const std::string& sep, It& v)
double atof(const char *s);
double atof(const std::string &s);
std::string base64_encode(unsigned char const *bytes_to_encode,
unsigned int in_len);
std::string base64_decode(std::string const &encoded_string);
} // namespace tinyusdz

View File

@@ -26,7 +26,8 @@ struct Context
// key = UUID
std::unordered_map<std::string, USDLayer> layers;
std::unordered_set<std::string> resources;
// key = URI, value = UUID
std::unordered_map<std::string, std::string> resources;
};
} // namespace mcp

View File

@@ -21,51 +21,55 @@ namespace mcp {
namespace {
static bool ListResourcesImpl(const Context &ctx, json &result) {
for (const auto &res_uuid : ctx.resources) {
result["resources"] = nlohmann::json::array();
for (const auto &res : ctx.resources) {
if (!ctx.layers.count(res_uuid)) {
if (!ctx.layers.count(res.second)) {
continue;
}
json res_j;
res_j["uri"] = ctx.layers.at(res_uuid).uri;
res_j["name"] = ctx.layers.at(res_uuid).uri; // FIXME
res_j["uri"] = ctx.layers.at(res.second).uri;
res_j["name"] = ctx.layers.at(res.second).uri; // FIXME
res_j["mimeType"] = "application/octet-stream"; // FIXME
// TODO: size, title, description
result["resources"].push_back(res_j);
}
return true;
}
} // namespace
bool GetResourcesList(const Context &ctx, nlohmann::json &result) {
return ListResourcesImpl(ctx, result);
}
bool ReadResource(const Context &ctx, const std::string &uuid, nlohmann::json &result) {
bool ReadResource(const Context &ctx, const std::string &uri, nlohmann::json &result) {
// TODO: multiple resources
(void)uuid;
(void)result;
if (!ctx.resources.count(uuid)) {
if (!ctx.resources.count(uri)) {
// TODO: report error
return false;
}
const std::string &uuid = ctx.resources.at(uri);
if (!ctx.layers.count(uuid)) {
// This should not happen though.
return false;
}
json res;
res["uri"] = ctx.layers.at(uuid).uri;
res["name"] = ctx.layers.at(uuid).name;
res["uri"] = uri;
res["name"] = uri; // FIXME
res["mimeType"] = "text/plain";
// TODO: title

View File

@@ -381,20 +381,20 @@ bool MCPServer::Impl::init(int port, const std::string &host) {
return {};
});
register_method("resources/list",[](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
register_method("resources/list",[this](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
(void)err;
(void)params;
(void)sess_id;
nlohmann::json j;
mcp::GetResourcesList(j);
mcp::GetResourcesList(mcp_ctx_, j);
return j;
});
register_method("resources/read",[](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
register_method("resources/read",[this](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
(void)err;
(void)params;
@@ -407,7 +407,7 @@ bool MCPServer::Impl::init(int port, const std::string &host) {
std::string uri = params["uri"];
nlohmann::json result;
if (!mcp::ReadResource(uri, result)) {
if (!mcp::ReadResource(mcp_ctx_, uri, result)) {
err += "Failed to read resource: " + uri;
return {};
}
@@ -416,14 +416,14 @@ bool MCPServer::Impl::init(int port, const std::string &host) {
});
register_method("tools/list",[](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
register_method("tools/list",[this](const nlohmann::json &params, const std::string &sess_id, std::string &err) -> nlohmann::json {
(void)err;
(void)params;
(void)sess_id;
nlohmann::json j;
mcp::GetToolsList(j);
mcp::GetToolsList(mcp_ctx_, j);
return j;

View File

@@ -1,9 +1,12 @@
#include <string>
#include "mcp-tools.hh"
#include "mcp-server.hh"
#include "mcp-context.hh"
#include <string>
#include "tinyusdz.hh"
#include "uuid-gen.hh"
#include "str-util.hh"
#include "pprinter.hh"
#ifdef __clang__
#pragma clang diagnostic push
@@ -21,8 +24,30 @@ namespace tydra {
namespace mcp {
namespace {
inline std::string decode_datauri(const std::string &data) {
const std::string prefix = "data:application/octet-stream;base64,";
if (!startsWith(data, prefix)) {
return {};
}
if (data.size() <= prefix.size()) {
return {};
}
// TODO: save memory
std::string binary = base64_decode(removePrefix(data, prefix));
return binary;
}
bool GetVersion(nlohmann::json &result);
bool LoadUSDLayerFromFile(Context &ctx, const nlohmann::json &args, nlohmann::json &result, std::string &err);
bool LoadUSDLayerFromDataURI(Context &ctx, const nlohmann::json &args, nlohmann::json &result, std::string &err);
bool GetVersion(nlohmann::json &result) {
@@ -83,7 +108,65 @@ bool LoadUSDLayerFromFile(Context &ctx, const nlohmann::json &args, nlohmann::js
usd_layer.layer = std::move(layer);
ctx.layers.emplace(uuid, std::move(usd_layer));
ctx.resources.insert(uuid);
ctx.resources.emplace(uri, uuid);
DCOUT("loaded USD as Layer");
nlohmann::json content;
content["type"] = "text";
content["text"] = uuid;
result["content"] = nlohmann::json::array();
result["content"].push_back(content);
return true;
}
bool LoadUSDLayerFromDataURI(Context &ctx, const nlohmann::json &args, nlohmann::json &result, std::string &err) {
DCOUT("args " << args);
if (!args.contains("uri")) {
DCOUT("uri param not found");
err = "`uri` param not found.\n";
return false;
}
if (!args.contains("name")) {
DCOUT("name param not found");
err = "`name` param not found.\n";
return false;
}
std::string name = args["name"];
std::string binary = decode_datauri(args.at("uri"));
Layer layer;
std::string warn;
USDLoadOptions options;
if (!LoadLayerFromMemory(reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(), name, &layer, &warn, &err, options)) {
DCOUT("Failed to load layer from DataURI: " << err);
err = "Failed to load layer from DataURI: " + err + "\n";
return false;
}
if (!warn.empty()) {
result["warnings"] = warn;
}
std::string uuid = generateUUID();
if (ctx.layers.count(uuid)) {
DCOUT("uuid conflict");
// This should not be happen.
err = "Internal error. UUID conflict\n";
return false;
}
USDLayer usd_layer;
usd_layer.uri = name;
usd_layer.layer = std::move(layer);
ctx.layers.emplace(uuid, std::move(usd_layer));
ctx.resources.emplace(name, uuid);
DCOUT("loaded USD as Layer");
@@ -127,9 +210,47 @@ bool ListPrimSpecs(Context &ctx, const nlohmann::json &args, nlohmann::json &res
return true;
}
bool ToUSDA(Context &ctx, const nlohmann::json &args, nlohmann::json &result, std::string &err) {
DCOUT("args " << args);
if (!args.contains("uri")) {
DCOUT("uri param not found");
err = "`uri` param not found.\n";
return false;
}
std::string uri = args.at("uri");
if (!ctx.resources.count(uri)) {
err = "Resource not found: " + uri + "\n";
return false;
}
std::string uuid = ctx.resources.at(uri);
if (!ctx.layers.count(uuid)) {
// This should not happen though.
err = "Internal error. No corresponding Layer found\n";
return false;
}
nlohmann::json content;
content["type"] = "text";
content["mimeType"] = "text/plain";
const Layer &layer = ctx.layers.at(uuid).layer;
std::string str = to_string(layer); // to USDA
content["text"] = str;
result["content"] = nlohmann::json::array();
result["content"].push_back(content);
return true;
}
} // namespace
bool GetToolsList(nlohmann::json &result) {
bool GetToolsList(Context &ctx, nlohmann::json &result) {
(void)ctx;
result["tools"] = nlohmann::json::array();
@@ -150,8 +271,8 @@ bool GetToolsList(nlohmann::json &result) {
{
nlohmann::json j;
j["name"] = "load_usd_layer";
j["description"] = "Load USD as Layer from URI(currently local file only)";
j["name"] = "load_usd_layer_from_file";
j["description"] = "Load USD as Layer from a file(only works in C++ native binary)";
nlohmann::json schema;
schema["type"] = "object";
@@ -166,6 +287,25 @@ bool GetToolsList(nlohmann::json &result) {
}
{
nlohmann::json j;
j["name"] = "load_usd_layer_from_datauri";
j["description"] = "Load USD as Layer from DataURI";
nlohmann::json schema;
schema["type"] = "object";
schema["properties"] = nlohmann::json::object();
schema["properties"]["uri"] ={{"type", "string"}};
schema["properties"]["name"] ={{"type", "string"}};
schema["required"] = nlohmann::json::array({"uri", "name"});
j["inputSchema"] = schema;
result["tools"].push_back(j);
}
{
nlohmann::json j;
j["name"] = "list_primspecs";
@@ -184,6 +324,24 @@ bool GetToolsList(nlohmann::json &result) {
}
{
nlohmann::json j;
j["name"] = "to_usda";
j["description"] = "Convert USD Layer to USDA text";
nlohmann::json schema;
schema["type"] = "object";
schema["properties"] = nlohmann::json::object();
schema["properties"]["uri"] ={{"type", "string"}};
schema["required"] = nlohmann::json::array({"uri"});
j["inputSchema"] = schema;
result["tools"].push_back(j);
}
std::cout << result << "\n";
return true;
@@ -195,9 +353,15 @@ bool CallTool(Context &ctx, const std::string &tool_name, const nlohmann::json &
if (tool_name == "get_version") {
return GetVersion(result);
} else if (tool_name == "load_usd_layer") {
DCOUT("load_usd_layer");
} else if (tool_name == "load_usd_layer_from_file") {
DCOUT("load_usd_layer_from_file");
return LoadUSDLayerFromFile(ctx, args, result, err);
} else if (tool_name == "to_usda") {
DCOUT("to_usda");
return ToUSDA(ctx, args, result, err);
} else if (tool_name == "load_usd_layer_from_datauri") {
DCOUT("load_usd_layer_datauri");
return LoadUSDLayerFromDataURI(ctx, args, result, err);
} else if (tool_name == "list_primspecs") {
DCOUT("list_primspecs");
return ListPrimSpecs(ctx, args, result, err);

View File

@@ -22,6 +22,7 @@ namespace mcp {
// for 'tools/list'
bool GetToolsList(
Context &ctx,
nlohmann::json &result);

View File

@@ -12,15 +12,27 @@
#include <chrono>
#include <thread>
#include "external/fast_float/include/fast_float/bigint.h"
//#include "external/fast_float/include/fast_float/bigint.h"
#include "tinyusdz.hh"
#include "pprinter.hh"
#include "tydra/render-data.hh"
#include "tydra/scene-access.hh"
#include "tydra/mcp-context.hh"
#include "tydra/mcp-resources.hh"
#include "tydra/mcp-tools.hh"
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Weverything"
#endif
#include "external/jsonhpp/nlohmann/json.hpp"
#ifdef __clang__
#pragma clang diagnostic pop
#endif
// Handling Asset
// Due to the limitatrion of C++(synchronous) initiated async file(fetch) read,
// We decided to fetch asset in JavaScript layer.
@@ -1149,36 +1161,125 @@ class TinyUSDZLoaderNative {
return val;
}
emscripten::val mcpToolsList() const {
emscripten::val val;
bool mcpCreateContext(const std::string &session_id) {
if (mcp_ctx_.count(session_id)) {
// Context already exists
return false;
}
// TODO
mcp_ctx_[session_id] = tinyusdz::tydra::mcp::Context();
mcp_session_id_ = session_id;
return val;
return true;
}
emscripten::val mcpToolsCall() const {
emscripten::val val;
bool mcpSelectContext(const std::string &session_id) {
if (!mcp_ctx_.count(session_id)) {
// Context does not exist
return false;
}
// TODO
mcp_session_id_ = session_id;
return val;
return true;
}
emscripten::val mcpResourcesList() const {
emscripten::val val;
// TODO
// return JSON string
std::string mcpToolsList() {
return val;
if (!mcp_ctx_.count(mcp_session_id_)) {
// TODO: better error message
return "{ \"error\": \"invalid session_id\"}";
}
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
tinyusdz::tydra::mcp::Context &ctx = mcp_global_ctx_;
nlohmann::json result;
if (!tinyusdz::tydra::mcp::GetToolsList(ctx, result)) {
std::cerr << "[tydra:mcp:GetToolsList] failed." << "\n";
// TODO: Report error more nice way.
}
std::string s_result = result.dump();
return s_result;
}
emscripten::val mcpResourcesRead() const {
emscripten::val val;
// args: JSON string
// return JSON string
std::string mcpToolsCall(const std::string &tool_name, const std::string &args) {
// TODO
if (!mcp_ctx_.count(mcp_session_id_)) {
// TODO: better error message
return "{ \"error\": \"invalid session_id\"}";
}
return val;
nlohmann::json j_args = nlohmann::json::parse(args);
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
auto &ctx = mcp_global_ctx_;
nlohmann::json result;
std::string err;
if (!tinyusdz::tydra::mcp::CallTool(ctx, tool_name, j_args, result, err)) {
// TODO: Report error more nice way.
std::cerr << "[tydra:mcp:CallTool]" << err << "\n";
}
std::string s_result = result.dump();
return s_result;
}
std::string mcpResourcesList() {
std::cout << "res list\n";
if (!mcp_ctx_.count(mcp_session_id_)) {
// TODO: better error message
return "{ \"error\": \"invalid session_id\"}";
}
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
auto &ctx = mcp_global_ctx_;
nlohmann::json result;
if (!tinyusdz::tydra::mcp::GetResourcesList(ctx, result)) {
// TODO: Report error more nice way.
std::cerr << "[tydra:mcp:ListResources] failed\n";
}
std::string s_result = result.dump();
return s_result;
}
std::string mcpResourcesRead(const std::string &uri) {
if (!mcp_ctx_.count(mcp_session_id_)) {
// TODO: better error message
return "{ \"error\": \"invalid session_id\"}";
}
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
auto &ctx = mcp_global_ctx_;
nlohmann::json content;
if (!tinyusdz::tydra::mcp::ReadResource(ctx, uri, content)) {
// TODO: Report error more nice way.
std::cerr << "[tydra:mcp:ReadResources] failed\n";
}
std::string s_content = content.dump();
return s_content;
}
// TODO: Deprecate
@@ -1243,6 +1344,12 @@ class TinyUSDZLoaderNative {
tinyusdz::tydra::RenderScene render_scene_;
tinyusdz::USDZAsset usdz_asset_;
EMAssetResolutionResolver em_resolver_;
// key = session_id
std::unordered_map<std::string, tinyusdz::tydra::mcp::Context> mcp_ctx_;
std::string mcp_session_id_;
tinyusdz::tydra::mcp::Context mcp_global_ctx_;
};
///
@@ -1522,6 +1629,8 @@ EMSCRIPTEN_BINDINGS(tinyusdz_module) {
// MCP
.function("mcpCreateContext", &TinyUSDZLoaderNative::mcpCreateContext)
.function("mcpSelectContext", &TinyUSDZLoaderNative::mcpSelectContext)
.function("mcpResourcesList", &TinyUSDZLoaderNative::mcpResourcesList)
.function("mcpResourcesRead", &TinyUSDZLoaderNative::mcpResourcesRead)
.function("mcpToolsList", &TinyUSDZLoaderNative::mcpToolsList)

View File

@@ -4,8 +4,10 @@
"": {
"name": "mcp-server",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
"@modelcontextprotocol/sdk": "^1.16.0",
"body-parser": "^2.2.0",
"path": "^0.12.7",
"uuid": "^11.1.0",
"vite": "^7.0.4",
},
"devDependencies": {
@@ -70,7 +72,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.15.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.16.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.0", "", { "os": "android", "cpu": "arm" }, "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg=="],
@@ -208,7 +210,7 @@
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
@@ -320,6 +322,8 @@
"util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@7.0.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA=="],
@@ -332,8 +336,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
"http-errors/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "mcp-server",
"module": "index.ts",
"module": "server-http.js",
"type": "module",
"private": true,
"scripts": {
@@ -17,7 +17,9 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"body-parser": "^2.2.0",
"path": "^0.12.7",
"uuid": "^11.1.0",
"vite": "^7.0.4"
}
}

View File

@@ -1,129 +1,219 @@
import express from "express";
import { randomUUID } from "node:crypto";
import * as bodyParser from "body-parser";
//import { randomUUID } from "node:crypto";
import { v4 as uuidv4 } from "uuid";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpError, ErrorCode, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, CompleteRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import initTinyUSDZNative from "tinyusdz/tinyusdz.js";
//import { TinyUSDZLoader } from "tinyusdz/TinyUSDLoader.js";
//import { TinyUSDZMCPServer } from "tinyusdz/TinyUSDMCPServer.js";
const portno = 8085;
const app = express();
app.use(express.json());
//app.use(express.json());
// Increase limit for larger requests(e.g. DataURI representation of USD file)
app.use(bodyParser.json({limit: '50mb'})); // Increase limit for larger requests
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); // Increase limit for larger requests
// Map to store transports by session ID
const transports = {};
const session = new Map();
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
console.log("-- post --");
console.log(req.headers);
console.log(req.body);
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] || undefined;
console.log("sessionId", sessionId);
let transport = null;
initTinyUSDZNative().then(function (TinyUSDZ) {
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
console.log("init");
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
const tusd = new TinyUSDZ.TinyUSDZLoaderNative();
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
console.log("-- post --");
console.log(req.headers);
console.log(req.body);
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] || undefined;
let transport = null;
console.log("sessionId", sessionId);
// Store the transport by session ID
transports[sessionId] = transport;
},
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
// locally, make sure to set:
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1'],
});
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => uuidv4(),
onsessioninitialized: (sessionId) => {
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = new McpServer({
name: "example-server",
version: "1.0.0"
});
// Store the transport by session ID
transports[sessionId] = transport;
server.registerTool("get_version",
{
title: "Get TinyUSDZ version",
description: "Get TinyUSDZ version",
inputSchema: {}
},
async ({ }) => {
return {
content: [
{
type: 'text',
text: "v0.9.0"
}
],
tusd.mcpCreateContext(sessionId);
},
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
// locally, make sure to set:
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1'],
});
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = new McpServer({
name: "tinyusdz-mcp-server",
version: "0.9.5"
});
server.server.registerCapabilities({
resources: {
listChanged: true
},
tools: {
listChanged: true
}
});
server.registerTool("add",
{
title: "Addition Tool",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const sessionId = server.server.transport.sessionId;
console.log("list resources", sessionId);
// ... set up server resources, tools, and prompts ...
tusd.mcpSelectContext(sessionId);
const resources_str = tusd.mcpResourcesList();
console.log("resources_str", resources_str);
// Connect to the MCP server
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
const j = JSON.parse(resources_str);
return j;
});
// Handle the request
await transport.handleRequest(req, res, req.body);
server.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
const sessionId = server.server.transport.sessionId;
console.log("read resource", sessionId);
const uri = request.params.uri;
console.log("uri", uri);
tusd.mcpSelectContext(sessionId);
const resources_str = tusd.mcpResourcesRead(uri);
console.log("resources_str", resources_str);
const j = JSON.parse(resources_str);
return j;
});
server.server.setRequestHandler(ListToolsRequestSchema, async () => {
const sessionId = server.server.transport.sessionId;
console.log("sessId", sessionId);
console.log("tusd", tusd);
tusd.mcpSelectContext(sessionId);
console.log("listtools");
const tools_str = tusd.mcpToolsList();
console.log("tools_str", tools_str);
const j = JSON.parse(tools_str)
return j;
});
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
console.log("request", request);
const sessionId = server.server.transport.sessionId;
console.log("sessId", sessionId);
console.log("tusd", tusd);
tusd.mcpSelectContext(sessionId);
const tool_name = request.params.name;
const args = JSON.stringify(request.params.arguments);
console.log("tool_name", tool_name);
console.log("args", args);
const result_str = tusd.mcpToolsCall(tool_name, args);
console.log("result_str", result_str);
const j = JSON.parse(result_str)
return j;
});
/*
server.registerTool("get_version",
{
title: "Get TinyUSDZ version",
description: "Get TinyUSDZ version",
inputSchema: {}
},
async ({ }) => {
return {
content: [
{
type: 'text',
text: "v0.9.0"
}
],
}
});
server.registerTool("load_usd_layer",
{
title: "Load USD as Layer from URI",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
*/
// ... set up server resources, tools, and prompts ...
// Connect to the MCP server
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);
// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);
console.log("localhost:" + portno.toString())
app.listen(portno);
}).catch((error) => {
console.error("Failed to initialize TinyUSDZLoader:", error);
});
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);
// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);
console.log("localhost:" + portno.toString())
app.listen(portno);