Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Model Context Protocol so any MCP-capable agent can drive them.
| Tool | Description |
|---|---|
| `action_new` | Idempotently create an API endpoint (`__main__.py` + module file). `public` defaults to true. |
| `action_invoke` | Run `ops action invoke <endpoint>` with `key=value` params. |
| `action_invoke` | Run `ops invoke <endpoint>` with `key=value` params and return the JSON result. |
| `action_requirements` | Add a library to an endpoint's `requirements.txt` (skips preinstalled libs). |
| `action_add_secret` | Wire a `.env` secret into an endpoint's context as `ctx.<SECRET>`. |
| `secret_status` | Check secret presence and endpoint bindings without reading its value. |
Expand Down
2 changes: 1 addition & 1 deletion spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Receive an <endpoint> (`package/action`) and a series of key=value pairs.

## executes

the command `ops action invoke <endpoint> -p "<key1>" "<value1>" ... -p "<keyn>" "<valuen>"`
the command `ops invoke <endpoint> -p "<key1>" "<value1>" ... -p "<keyn>" "<valuen>"`

## returns

Expand Down
6 changes: 3 additions & 3 deletions src/tools/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { error, text, defineTool } from "../lib.ts"
export default defineTool({
name: "action_invoke",
config: {
description: "Invoke an API action. Executes `ops action invoke` with the given endpoint and key=value parameters.",
description: "Invoke an API action and return its JSON result. Executes `ops invoke` with the given endpoint and key=value parameters.",
inputSchema: {
endpoint: z.string().describe("The endpoint path: 'package/action'"),
params: z
Expand All @@ -43,7 +43,7 @@ export default defineTool({
}
}

const argv = ["action", "invoke", endpoint.trim(), ...paramArgs]
const argv = ["invoke", endpoint.trim(), ...paramArgs]

const { stdout, stderr, exitCode } = await new Promise<{
stdout: string
Expand All @@ -62,6 +62,6 @@ export default defineTool({
if (exitCode !== 0) result += `\nExit code: ${exitCode}`
return exitCode === 0
? text(result || "(no output)")
: error(result || `ops action invoke failed with exit code ${exitCode}`)
: error(result || `ops invoke failed with exit code ${exitCode}`)
},
})
51 changes: 51 additions & 0 deletions tests/invoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.

import assert from "node:assert/strict"
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { delimiter, join } from "node:path"
import test from "node:test"
import actionInvoke from "../src/tools/invoke.ts"

function resultText(result: { content: { text: string }[] }): string {
return result.content.map((part) => part.text).join("\n")
}

test("action_invoke returns the action result instead of an activation id", async () => {
const directory = mkdtempSync(join(tmpdir(), "openserverless-mcp-invoke-test-"))
const bin = join(directory, "bin")
const ops = join(bin, "ops")
const previousPath = process.env.PATH
mkdirSync(bin)
writeFileSync(ops, `#!/usr/bin/env node
const args = process.argv.slice(2)
const resultOnly = args[0] === "invoke" || args.includes("-r")
process.stdout.write(resultOnly ? '{"answer":42}\\n' : "ok: invoked /_/v1/demo with id activation-123\\n")
`)
chmodSync(ops, 0o755)

try {
process.env.PATH = `${bin}${delimiter}${previousPath ?? ""}`
const result = await actionInvoke.handler({ endpoint: "v1/demo", params: ["key=value"] })
assert.equal(result.isError, undefined)
assert.equal(resultText(result), '{"answer":42}\n')
} finally {
process.env.PATH = previousPath
rmSync(directory, { recursive: true, force: true })
}
})
Loading