feat(apisix): add Cloudron package
- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local unpack = unpack
|
||||
local ngx = ngx
|
||||
local thread_spawn = ngx.thread.spawn
|
||||
local thread_kill = ngx.thread.kill
|
||||
local worker_exiting = ngx.worker.exiting
|
||||
local resty_signal = require("resty.signal")
|
||||
local core = require("apisix.core")
|
||||
local pipe = require("ngx.pipe")
|
||||
|
||||
local mcp_server_wrapper = require("apisix.plugins.mcp.server_wrapper")
|
||||
|
||||
local schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
base_uri = {
|
||||
type = "string",
|
||||
minLength = 1,
|
||||
default = "",
|
||||
},
|
||||
command = {
|
||||
type = "string",
|
||||
minLength = 1,
|
||||
},
|
||||
args = {
|
||||
type = "array",
|
||||
items = {
|
||||
type = "string",
|
||||
},
|
||||
minItems = 0,
|
||||
},
|
||||
},
|
||||
required = {
|
||||
"command"
|
||||
},
|
||||
}
|
||||
|
||||
local plugin_name = "mcp-bridge"
|
||||
|
||||
local _M = {
|
||||
version = 0.1,
|
||||
priority = 510,
|
||||
name = plugin_name,
|
||||
schema = schema,
|
||||
}
|
||||
|
||||
|
||||
function _M.check_schema(conf, schema_type)
|
||||
return core.schema.check(schema, conf)
|
||||
end
|
||||
|
||||
|
||||
local function on_connect(conf, ctx)
|
||||
return function(additional)
|
||||
local proc, err = pipe.spawn({conf.command, unpack(conf.args or {})})
|
||||
if not proc then
|
||||
core.log.error("failed to spawn mcp process: ", err)
|
||||
return 500
|
||||
end
|
||||
proc:set_timeouts(nil, 100, 100)
|
||||
ctx.mcp_bridge_proc = proc
|
||||
|
||||
local server = additional.server
|
||||
|
||||
-- ngx_pipe is a yield operation, so we no longer need
|
||||
-- to explicitly yield to other threads by ngx_sleep
|
||||
ctx.mcp_bridge_proc_event_loop = thread_spawn(function ()
|
||||
local stdout_partial, stderr_partial, need_exit
|
||||
while not worker_exiting() do
|
||||
-- read all the messages in stdout's pipe, line by line
|
||||
-- if there is an incomplete message it is buffered and
|
||||
-- spliced before the next message
|
||||
repeat
|
||||
local line, _
|
||||
line, _, stdout_partial = proc:stdout_read_line()
|
||||
if line then
|
||||
local ok, err = server.transport:send(
|
||||
stdout_partial and stdout_partial .. line or line
|
||||
)
|
||||
if not ok then
|
||||
core.log.info("session ", server.session_id,
|
||||
" exit, failed to send response message: ", err)
|
||||
need_exit = true
|
||||
break
|
||||
end
|
||||
stdout_partial = nil -- luacheck: ignore
|
||||
end
|
||||
until not line
|
||||
if need_exit then
|
||||
break
|
||||
end
|
||||
|
||||
repeat
|
||||
local line, _
|
||||
line, _, stderr_partial = proc:stderr_read_line()
|
||||
if line then
|
||||
local ok, err = server.transport:send(
|
||||
'{"jsonrpc":"2.0","method":"notifications/stderr","params":{"content":"'
|
||||
.. (stderr_partial and stderr_partial .. line or line) .. '"}}')
|
||||
if not ok then
|
||||
core.log.info("session ", server.session_id,
|
||||
" exit, failed to send response message: ", err)
|
||||
need_exit = true
|
||||
break
|
||||
end
|
||||
stderr_partial = "" -- luacheck: ignore
|
||||
end
|
||||
until not line
|
||||
if need_exit then
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function on_client_message(conf, ctx)
|
||||
return function(message, additional)
|
||||
core.log.info("session ", additional.server.session_id,
|
||||
" send message to mcp server: ", additional.raw)
|
||||
ctx.mcp_bridge_proc:write(additional.raw .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function on_disconnect(conf, ctx)
|
||||
return function()
|
||||
if ctx.mcp_bridge_proc_event_loop then
|
||||
thread_kill(ctx.mcp_bridge_proc_event_loop)
|
||||
ctx.mcp_bridge_proc_event_loop = nil
|
||||
end
|
||||
|
||||
local proc = ctx.mcp_bridge_proc
|
||||
if proc then
|
||||
proc:shutdown("stdin")
|
||||
proc:wait()
|
||||
local _, err = proc:wait() -- check if process not exited then kill it
|
||||
if err ~= "exited" then
|
||||
proc:kill(resty_signal.signum("KILL") or 9)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function _M.access(conf, ctx)
|
||||
return mcp_server_wrapper.access(conf, ctx, {
|
||||
event_handler = {
|
||||
on_connect = on_connect(conf, ctx),
|
||||
on_client_message = on_client_message(conf, ctx),
|
||||
on_disconnect = on_disconnect(conf, ctx),
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
return _M
|
Reference in New Issue
Block a user