Koskamo

Pushary MCP

Pushary is a human-in-the-loop MCP server that enables AI agents to ask people for approvals or answers via push notifications. It allows agents to wait for explicit Approve or Deny responses before executing protected actions, ensuring human oversight for critical decisions like refunds or deletions.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

Pushary MCP logo, by RalphNex OÜCommunity listingStreamable HTTPNo sign-inEndpoint checked 2026-11-07Published by RalphNex OÜ
MCP endpoint (Streamable HTTP)
https://pushary.com/api/mcp/mcp
Vendor documentation

What its tools do

6 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.

Read-only (1)

6 of 6 have a description · 6 of 6 declare behavior hints · 5 of 6 with parameters document them all

Checks and signals

Each line is separate evidence, not a trust score.

  • Passed Endpoint answered the live check ·
  • Availability 1 of 1 checks passed in 30 days

What you can do with it

  • Ask a person for approval before executing a sensitive action
  • Receive push notifications on mobile devices for agent decisions
  • Gate protected actions on human approval results
  • Maintain an audit trail of who answered and how

Source and maintenance

Community listingRegistry 1.5.2
Maintenance checked

Protocol and capabilities

Server
Pushary 1.0.0
MCP protocol negotiated in check
2025-11-25
Tools
6 advertised
Resources
5 advertised
Resource templates
0 advertised
Prompts
Not advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": false
  },
  "resources": {
    "listChanged": false
  }
}

Server-provided instructions

Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.

Honor authorization already granted by the user and the host permission policy. Use ask_user only for an unresolved decision or missing input; a multi-step task alone needs no extra approval. Ask once on one channel. Follow handoffAction when present, otherwise nextAction; silence is never consent. A poll ending does not expire a question. While pending, keep waiting with the same correlationId; never cancel merely because no answer arrived during a poll. Cancel a live question before moving it to the current client. If cancellation returns handoffAction "stop", stop. Otherwise, if cancellation returns false, poll once for 1 second and honor the answer that won the race. Use send_notification for a requested update or a meaningful unattended result, such as a finished task, not duplicate reports. Ask questions with ask_user, not with send_notification context.askQuestion. A question asked through a connector, by signing in or with a connector key, closes 10 minutes after it is sent; one asked with any other key stays open until answered or cancelled. If you would take your default without asking at all, do not ask: take it and list that decision in your final message. This never covers an irreversible step, a deploy or publish, anything sent outside the team, spending money or a decision outside your authority that the user has not already approved: those still need a question before you act; after a Pushary hook approved that exact tool call, do not ask about it again. Running the same command later is a new step. When the requested work is complete, report it and stop: do not ask what to do next or offer more work, through ask_user or context.askQuestion, unless the user asked you to. Work blocked on a choice only the user can make is not complete; ask that choice. Pass agentName as the actual host and task. Pushary skills guide use; tools do not override host approvals.

Tools

6 advertised

schedule_reminder — Schedule a Personal Reminder
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Schedule, list, retrieve or cancel a personal Pushary reminder for the authenticated user. Scheduled reminders can arrive after the agent exits. One-time only; up to 30 days away, 25 pending per user/site. Scheduling requires body and either inMinutes or at with an explicit UTC offset. requestId identifies one schedule across retries. reminderId retrieves its outcome; no arguments lists pending reminders; cancelReminderId cancels a pending reminder. Returns the scheduled time, state and any delivery warning. Delivery respects stop/mute and active membership. Does not schedule agent work or customer campaigns. API reference: https://pushary.com/docs/agents/reference/tools#schedule_reminder

schedule_reminder input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
      "description": "Future ISO 8601 timestamp with explicit UTC offset or Z, at most 30 days away. Resolve the user's local time and timezone before scheduling; never guess a timezone."
    },
    "env": {
      "type": "string",
      "const": "test"
    },
    "body": {
      "type": "string",
      "maxLength": 180,
      "minLength": 1,
      "description": "What to remind the authenticated user about, at most 180 UTF-16 units after secret redaction (emoji may use two). Omit all fields to list their pending reminders."
    },
    "title": {
      "type": "string",
      "maxLength": 100,
      "minLength": 1
    },
    "agentName": {
      "type": "string",
      "maxLength": 100
    },
    "inMinutes": {
      "type": "integer",
      "maximum": 43200,
      "minimum": 1,
      "description": "Minutes from the server clock; use for timers and relative reminders."
    },
    "machineId": {
      "type": "string",
      "maxLength": 128
    },
    "requestId": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Unique ID for this scheduling operation. Reuse it unchanged on retries to return the original reminder, including after it settles."
    },
    "sessionId": {
      "type": "string",
      "maxLength": 128
    },
    "reminderId": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Look up your reminder, including its final outcome and delivery status, instead of scheduling or cancelling."
    },
    "cancelReminderId": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Cancel this user's scheduled reminder instead of creating one. Already-dispatching reminders cannot be cancelled."
    }
  }
}
schedule_reminder annotations
{
  "title": "Schedule a Personal Reminder",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
send_notification — Send Push Notification
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Send a notification to connected devices for a requested update or a meaningful unattended result, such as a finished task. Delivery follows the account policy and optional recipient filters. context adds a detail page. context.askQuestion is kept for older clients; ask questions with ask_user. Returns web/mobile delivery counts and a warning when no delivery channel is connected. Sends real notifications. API reference: https://pushary.com/docs/agents/reference/tools#send_notification

send_notification input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "title",
    "body"
  ],
  "properties": {
    "env": {
      "enum": [
        "test"
      ],
      "type": "string",
      "description": "Set to \"test\" from a test suite. The notification is recorded in the activity feed and nothing is delivered to a phone or browser. The X-Pushary-Env: test header does the same for every call on the connection."
    },
    "url": {
      "type": "string",
      "format": "uri",
      "description": "URL opened when the user taps the notification. Ignored if context is provided, because a context detail page URL is generated automatically."
    },
    "body": {
      "type": "string",
      "maxLength": 500,
      "minLength": 1,
      "description": "Notification body text (max 500 chars). One or two sentences the user can act on without opening anything."
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to subscribers that have any of these tags."
    },
    "title": {
      "type": "string",
      "maxLength": 100,
      "minLength": 1,
      "description": "Notification title shown on the lock screen (max 100 chars). Lead with the outcome, e.g. \"Build finished\" or \"Migration failed\"."
    },
    "context": {
      "type": "object",
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "enum": [
            "task_complete",
            "error",
            "info"
          ],
          "type": "string",
          "description": "What kind of update this is. Use \"task_complete\" when work finished, \"error\" when something failed (delivered with high urgency), \"info\" for everything else. Error severity describes this notification, not the agent session health."
        },
        "details": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Bullet-point details rendered as a list"
        },
        "summary": {
          "type": "string",
          "description": "Short summary of what happened, shown at the top of the detail page"
        },
        "errorFile": {
          "type": "string",
          "description": "File path where the error occurred"
        },
        "nextSteps": {
          "type": "string",
          "description": "What the user should do next, e.g. \"Review the PR\" or \"Re-run with --force\""
        },
        "askQuestion": {
          "type": "object",
          "required": [
            "question"
          ],
          "properties": {
            "type": {
              "enum": [
                "confirm",
                "select",
                "input"
              ],
              "type": "string",
              "default": "confirm",
              "description": "Question type: confirm (yes/no), select (pick from options), or input (free text)"
            },
            "options": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 6,
              "minItems": 2,
              "description": "The 2 to 6 choices for a select question"
            },
            "question": {
              "type": "string",
              "maxLength": 8000,
              "minLength": 1,
              "description": "A follow-up question shown below the context (e.g. \"Retry with a different approach?\")"
            }
          },
          "description": "Kept for older clients. To ask the user something, call ask_user instead: it stays open at least as long, offers lock-screen answers on confirm questions, and, unless the agent connects through a connector, stays open until answered or cancelled. An embedded question appears on the detail page, closes 10 minutes after it is created, and returns a linkedCorrelationId for wait_for_answer. Only for a decision about unfinished work. Not for asking what to do next once the requested work is complete, unless the user asked you to."
        },
        "errorMessage": {
          "type": "string",
          "description": "The error message, when type is \"error\""
        },
        "filesChanged": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Paths of files that were created or modified"
        }
      },
      "description": "Structured context rendered as a rich detail page when the user taps the notification. Strongly recommended for task_complete and error notifications so the user can act from their phone."
    },
    "iconUrl": {
      "type": "string",
      "format": "uri",
      "description": "URL of the notification icon image"
    },
    "imageUrl": {
      "type": "string",
      "format": "uri",
      "description": "URL of a large image shown in the notification"
    },
    "agentName": {
      "type": "string",
      "maxLength": 100,
      "description": "Name of the agent sending this notification, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification so the user knows which session is talking. Falls back to the MCP client name if omitted."
    },
    "machineId": {
      "type": "string",
      "maxLength": 128,
      "description": "Stable machine id of the sending agent, so two machines never collapse into one session."
    },
    "sessionId": {
      "type": "string",
      "maxLength": 128,
      "description": "Opaque per-session id of the sending agent, so parallel sessions are attributed separately in the activity feed."
    },
    "externalIds": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to subscribers matching these external IDs."
    },
    "subscriberIds": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device."
    }
  }
}
send_notification output schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "env": {
      "enum": [
        "test"
      ],
      "type": "string",
      "description": "Echoed when the call was test traffic."
    },
    "hint": {
      "type": "string",
      "description": "What to do next, when there is a next step."
    },
    "sent": {
      "type": "number",
      "description": "Total devices reached, web plus mobile. Zero is a successful call that found nobody to deliver to, not an error."
    },
    "status": {
      "enum": [
        "recorded",
        "ignored"
      ],
      "type": "string",
      "description": "Outcome when a session completion is recorded for delivery once per turn."
    },
    "updates": {
      "type": "object",
      "required": [
        "mode",
        "source"
      ],
      "properties": {
        "mode": {
          "enum": [
            "both",
            "phone",
            "browser",
            "none"
          ],
          "type": "string"
        },
        "source": {
          "enum": [
            "kill",
            "snooze",
            "setting",
            "mode",
            "default"
          ],
          "type": "string"
        }
      },
      "description": "Effective update delivery setting and the policy that selected it.",
      "additionalProperties": {}
    },
    "warning": {
      "type": "string",
      "description": "Present only when the notification reached zero devices, naming what the user has to connect."
    },
    "delivery": {
      "type": "object",
      "required": [
        "web",
        "mobile"
      ],
      "properties": {
        "web": {
          "type": "object",
          "required": [
            "recipients"
          ],
          "properties": {
            "status": {
              "type": "string",
              "description": "Why web delivery reached nobody, present only when it reached nobody."
            },
            "recipients": {
              "type": "number",
              "description": "Browsers that accepted the push."
            }
          },
          "additionalProperties": {}
        },
        "mobile": {
          "type": "object",
          "required": [
            "recipients"
          ],
          "properties": {
            "status": {
              "type": "string",
              "description": "Why mobile delivery reached nobody, present only when it reached nobody."
            },
            "recipients": {
              "type": "number",
              "description": "Phones that accepted the push."
            }
          },
          "additionalProperties": {}
        }
      },
      "description": "Per-channel outcome. The two channels are independent with no cross-fallback, so each reports its own result.",
      "additionalProperties": {}
    },
    "suppressed": {
      "type": "string",
      "description": "Reason the notification was recorded without a push."
    },
    "attribution": {
      "enum": [
        "session",
        "unattributed"
      ],
      "type": "string",
      "description": "Whether real session and machine identifiers were supplied. Unattributed notifications are delivered without creating a live session."
    },
    "completionId": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "description": "Recorded completion identifier, or null when no completion was recorded."
    },
    "notificationId": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Trace ID for this notification; provider acceptance does not confirm a person saw it."
    },
    "linkedCorrelationId": {
      "type": "string",
      "description": "Present only when context.askQuestion embedded a decision prompt. Pass it to wait_for_answer to collect the response."
    }
  },
  "additionalProperties": {}
}
send_notification annotations
{
  "title": "Send Push Notification",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
ask_user — Ask User a Question
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Request an unresolved decision or missing input from the user through Pushary. Supports confirm, select and input questions. Delivery and waiting follow the account policy; a call waits at most 48 seconds and may return immediately. Returns the question state, answer when available, delivery information and handoff fields. answerUrl is optional. A pending question remains answerable after a poll ends. Sends a real question; it does not grant permission for other actions.

ask_user input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "question"
  ],
  "properties": {
    "env": {
      "enum": [
        "test"
      ],
      "type": "string",
      "description": "Set to \"test\" from a test suite. The question is stored and returned as pending, and nothing is delivered to a phone, browser or Slack. The X-Pushary-Env: test header does the same for every call on the connection."
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to subscribers that have any of these tags."
    },
    "type": {
      "enum": [
        "confirm",
        "select",
        "input"
      ],
      "type": "string",
      "default": "confirm",
      "description": "Question type: confirm renders yes/no buttons, select renders the options list, input renders a free-text field."
    },
    "wait": {
      "type": "boolean",
      "default": true,
      "description": "true (default) blocks until the user answers or the timeout fires. Set false to return immediately with a pending correlationId and poll it yourself via wait_for_answer."
    },
    "action": {
      "type": "string",
      "minLength": 0,
      "description": "The concrete operation about to happen, one line. Shown as the Action line. Cut to 500 chars."
    },
    "intent": {
      "type": "string",
      "minLength": 0,
      "description": "The user's stated task (from their last prompt), one line. Shown as the Intent line so the user can see why the agent stopped. Cut to 500 chars."
    },
    "blocker": {
      "type": "string",
      "minLength": 0,
      "description": "The single gating reason the agent stopped, one line. Shown as the Blocker line. Cut to 500 chars."
    },
    "context": {
      "type": "string",
      "maxLength": 500,
      "description": "One or two sentences about what the agent is working on, shown above the question so the user can decide without opening the terminal."
    },
    "options": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 6,
      "minItems": 2,
      "description": "The 2 to 6 choices for a select question. Required when type is \"select\", ignored otherwise. The answered value is the chosen option string."
    },
    "repoKey": {
      "type": "string",
      "maxLength": 200,
      "description": "Stable repository identity for the working directory, e.g. \"github.com/acme/api\". Lets an approval routing rule scoped to one repository avoid governing another. Optional; omit it and only workspace-wide routing rules apply."
    },
    "question": {
      "type": "string",
      "maxLength": 8000,
      "minLength": 1,
      "description": "The complete question, up to 8000 characters. Preserve every point. Notifications show a short preview; the answer interface shows the full question."
    },
    "toolName": {
      "type": "string",
      "maxLength": 100,
      "description": "The tool this approval is for (e.g. \"Bash\"), so the user can choose to always-allow it."
    },
    "toolPath": {
      "type": "string",
      "format": "starts_with",
      "pattern": "^\\/.*",
      "maxLength": 4096,
      "description": "MACHINE-POPULATED. Exact absolute Write file_path from the runtime, for diagnostic correlation only. Models must omit it."
    },
    "agentName": {
      "type": "string",
      "minLength": 0,
      "description": "Name of the agent asking, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification title so the user knows which session needs them. Falls back to the MCP client name if omitted. Cut to 100 chars."
    },
    "machineId": {
      "type": "string",
      "maxLength": 128,
      "description": "Stable machine id of the asking agent, so two machines never collapse into one session."
    },
    "questions": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "question",
          "multiSelect",
          "options"
        ],
        "properties": {
          "header": {
            "type": "string",
            "maxLength": 40
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "label"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "maxLength": 100,
                  "minLength": 1
                },
                "description": {
                  "type": "string",
                  "maxLength": 500
                }
              }
            },
            "maxItems": 4,
            "minItems": 2
          },
          "question": {
            "type": "string",
            "maxLength": 8000,
            "minLength": 1
          },
          "allowOther": {
            "type": "boolean"
          },
          "multiSelect": {
            "type": "boolean"
          }
        }
      },
      "maxItems": 1,
      "minItems": 1,
      "description": "ONE question, in the richer Claude-compatible shape: a header, per-option descriptions, multiSelect, and an optional write-in. Exactly one keeps already-installed clients answerable; asking several means several calls. Runtime-populated; ordinary callers should omit it and use question/type/options."
    },
    "requestId": {
      "type": "string",
      "maxLength": 200,
      "description": "MACHINE-POPULATED. The CALLER's own identifier for one logical invocation, used only when the runtime supplies no toolUseId. Mint it once, outside your retry loop, and send the same value on every attempt, so three retries of one ask become one decision. Do NOT derive it from the question text or reuse it across two deliberate asks: both collapse a real second question into the first one's answer. If you are a model deciding to call this tool, omit this field."
    },
    "scopePath": {
      "type": "string",
      "maxLength": 200,
      "description": "Set ONLY when this approval exists because the path falls outside the scope the user ratified via propose_scope. Approving then widens the run scope to include this exact path, so the user is not asked again for the same area."
    },
    "sessionId": {
      "type": "string",
      "maxLength": 128,
      "description": "Opaque per-session id of the asking agent, so parallel sessions are attributed separately."
    },
    "timeoutMs": {
      "type": "integer",
      "maximum": 55000,
      "minimum": 1000,
      "description": "How long this call blocks, in milliseconds (up to 55000 is accepted; one call blocks at most 48 seconds). Defaults to the site policy timeout. Ordinary questions stay open until answered or cancelled; through a connector they close 10 minutes after they are sent. A timeout ends this poll only; follow up with wait_for_answer."
    },
    "toolUseId": {
      "type": "string",
      "maxLength": 200,
      "description": "MACHINE-POPULATED. The agent RUNTIME's own identifier for the tool call this approval gates, forwarded verbatim by a hook that received it. Do NOT invent, guess, derive, or reuse a value: two different questions sent under the same id collapse into one, and the second one never reaches a human. If you are a model deciding to call this tool, omit this field."
    },
    "actionBody": {
      "type": "string",
      "maxLength": 4000,
      "description": "The diff (Edit/Write) or full command (Bash/apply_patch), secret-redacted and size-capped. Rendered as a collapsible detail block; never used as the push body."
    },
    "toolTarget": {
      "type": "string",
      "maxLength": 80,
      "description": "Compact target of the tool call (e.g. the command head \"git push\" for Bash, or a file extension like \".ts\" for Edit/Write). Used to mine policy suggestions."
    },
    "waitEndsAt": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
      "description": "MACHINE-POPULATED. When the agent hook stops waiting live and hands control back to the terminal. The question may remain answerable after this time. Ordinary callers should omit it."
    },
    "callbackUrl": {
      "type": "string",
      "format": "uri",
      "description": "Webhook URL that receives a POST with the answer when the user responds, signed with the X-Pushary-Signature header. Useful when the agent process may exit before the answer arrives."
    },
    "externalIds": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to subscribers matching these external IDs."
    },
    "pausedAware": {
      "type": "boolean",
      "description": "MACHINE-POPULATED. true from a hook that ends a paused approval exactly as the same approval unanswered after its full wait. Any other value is ignored. If you are a model deciding to call this tool, omit this field."
    },
    "placeholder": {
      "type": "string",
      "maxLength": 200,
      "description": "Hint text shown inside the free-text field for input questions"
    },
    "questionOwner": {
      "type": "object",
      "properties": {},
      "description": "MACHINE-POPULATED. Ownership supplied by the supported personal Codex bridge. Models must omit this field.",
      "additionalProperties": {}
    },
    "subscriberIds": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device."
    },
    "permissionMode": {
      "type": "string",
      "maxLength": 64,
      "description": "MACHINE-POPULATED. The agent runtime's own permission or approval mode for the tool call this approval gates, recorded with the decision. Any other value is ignored. If you are a model deciding to call this tool, omit this field."
    },
    "sessionToolAware": {
      "type": "boolean",
      "description": "MACHINE-POPULATED. Set by a Pushary hook whose engine applies a session tool grant with its risky-command ceiling, so the grant is only offered where it takes effect. If you are a model deciding to call this tool, omit this field."
    },
    "localSurfaceAvailable": {
      "type": "boolean",
      "description": "MACHINE-POPULATED. false from a hook that cannot show its own approval prompt for this call, so the phone is asked every time and never spared for presence at the keyboard. Any other value is ignored. If you are a model deciding to call this tool, omit this field."
    }
  }
}
ask_user output schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "correlationId",
    "question",
    "type"
  ],
  "properties": {
    "env": {
      "enum": [
        "test"
      ],
      "type": "string",
      "description": "Echoed when the call was test traffic."
    },
    "held": {
      "enum": [
        "test_traffic",
        "unattributed"
      ],
      "type": "string",
      "description": "Present when the question was stored but deliberately not delivered: test traffic, or a permission ask with no toolName and no sessionId."
    },
    "hint": {
      "type": "string",
      "description": "What to do next, when there is a next step."
    },
    "mode": {
      "enum": [
        "notify_only",
        "terminal_only"
      ],
      "type": "string",
      "description": "The site delivery mode that stopped this call from waiting."
    },
    "note": {
      "type": "string",
      "description": "Free text the user added alongside their answer."
    },
    "type": {
      "enum": [
        "confirm",
        "select",
        "input"
      ],
      "type": "string",
      "description": "The question type that was rendered."
    },
    "value": {
      "type": "string",
      "description": "The user's answer: \"yes\" or \"no\" for confirm, the chosen option for select, the typed text for input."
    },
    "paused": {
      "type": "boolean",
      "description": "True when nothing was sent because the last approvals in this session went unanswered. Withdraw the question, then end the call as it would end unanswered."
    },
    "status": {
      "enum": [
        "answered",
        "pending",
        "cancelled",
        "expired",
        "missing",
        "unavailable",
        "notified",
        "terminal",
        "stopped"
      ],
      "type": "string",
      "description": "The question state. Only pending is a live unanswered wait; cancelled, expired, missing, and unavailable must not be described as timeouts."
    },
    "warning": {
      "type": "string",
      "description": "Present only when no channel is connected, naming what the user has to connect."
    },
    "answered": {
      "type": "boolean",
      "description": "True once the user responded. Absent on the wait:false path, where nothing was awaited."
    },
    "delivery": {
      "type": "object",
      "required": [
        "web",
        "mobile"
      ],
      "properties": {
        "web": {
          "type": "number",
          "description": "Browsers the question reached."
        },
        "mobile": {
          "type": "number",
          "description": "Phones the question reached."
        },
        "pending": {
          "type": "boolean",
          "description": "True when delivery was still in flight when this returned, so the counts above are not final."
        }
      },
      "description": "Per-channel reach for the push carrying this question.",
      "additionalProperties": {}
    },
    "question": {
      "type": "string",
      "description": "The question exactly as the user saw it."
    },
    "routedTo": {
      "type": "string",
      "description": "Who the enforced route sends this decision to, for display, when requesterMayAnswer is false."
    },
    "timedOut": {
      "type": "boolean",
      "description": "True when the initial wait ended while the question was still live. Poll once with wait_for_answer, then follow handoffAction when present, otherwise nextAction."
    },
    "answerUrl": {
      "type": "string",
      "description": "Optional signed-in dashboard page where the user can answer this question."
    },
    "noDevices": {
      "type": "boolean",
      "description": "True when no phone, browser, or Slack channel could receive the question. Do not wait; follow handoffAction immediately."
    },
    "nextAction": {
      "enum": [
        "wait_for_answer",
        "ask_in_current_client"
      ],
      "type": "string",
      "description": "Backward-compatible next step: keep waiting while pending or ask in the current client. Follow handoffAction first when present."
    },
    "suppressed": {
      "type": "boolean",
      "description": "True when the PHONE push was deliberately held because a terminal on this machine is active, or, alongside paused, because nothing was sent at all. It says nothing about the notch, the dashboard or Slack, which are unaffected and may still be showing this question. A caller with no screen of its own should treat it as the terminal's to answer; a caller that can render the question itself should keep waiting unless paused is true."
    },
    "waitEndsAt": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
      "description": "When the agent hook stops waiting live. On an idempotent replay this is the original question's deadline, which the hook must reuse."
    },
    "answerSource": {
      "enum": [
        "dashboard",
        "mobile_app",
        "mobile_background",
        "decide_page",
        "live_page",
        "answer_link",
        "inbox",
        "slack",
        "mac_app",
        "sandbox"
      ],
      "type": "string",
      "description": "Recorded answering surface, when known. Missing provenance is not proof of a phone answer; sandbox is simulated."
    },
    "deliveryMode": {
      "enum": [
        "push_first",
        "push_only",
        "notify_only",
        "terminal_only"
      ],
      "type": "string",
      "description": "Effective delivery policy for this decision."
    },
    "correlationId": {
      "type": "string",
      "description": "Id of the question that was created. Pass it to wait_for_answer to keep waiting, or to cancel_question to retract it."
    },
    "handoffAction": {
      "enum": [
        "cancel_then_ask_in_current_client",
        "stop"
      ],
      "type": "string",
      "description": "Race-safe directive for updated clients. Takes precedence over nextAction: cancel before asking in the current client, or stop the handoff."
    },
    "policyTimeoutMs": {
      "type": "number",
      "minimum": 0,
      "description": "Policy wait window in milliseconds. Callers must also honor their host deadline."
    },
    "expiresInSeconds": {
      "type": "number",
      "description": "How long the question stays answerable."
    },
    "requesterMayAnswer": {
      "type": "boolean",
      "description": "False when a Team approval route that enforces who answers sends this decision to other people. Do not answer it in the current client or hand it to a local prompt: only the route's approvers can answer it."
    }
  },
  "additionalProperties": {}
}
ask_user annotations
{
  "title": "Ask User a Question",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
propose_scope — Propose Run Scope
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Request agreement on an unresolved or user-requested boundary for this session. Sends a real scope question and returns ratified, answered, contract and enforcement information. Only supported hooks can enforce file paths on path-bearing tool calls; promises are recorded but not enforced. Empty enforces means no automatic boundary checking. A contract never overrides host permissions or authorizes a separate action. The call waits at most 48 seconds; a pending proposal remains answerable after the call returns.

propose_scope input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "doneWhen",
    "sessionId"
  ],
  "properties": {
    "doneWhen": {
      "type": "string",
      "maxLength": 300,
      "minLength": 1,
      "description": "What \"finished\" means for this run, one or two lines. Carried for the human to judge against; never enforced automatically."
    },
    "promises": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "minLength": 1
      },
      "maxItems": 10,
      "description": "Boundaries that are not file paths: recipients, channels, spend limits, systems you will not open. For an agent whose work is not code (marketing, sales, support, operations), this is where the boundary goes. Shown to the user labelled \"Promised, not checked\" and recorded in the ledger, but NEVER enforced, because the gate judges a file path and these have none. Do not put these in allowedPaths."
    },
    "agentName": {
      "type": "string",
      "maxLength": 100,
      "description": "Name of the agent asking, format \"{Agent} - {project}\"."
    },
    "machineId": {
      "type": "string",
      "maxLength": 128,
      "description": "Stable machine id, so two machines never collapse into one session."
    },
    "sessionId": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Your per-session id, as your client reports it for THIS run. Required, and it is the key the gate reads the contract back by: a value that matches no live session still returns ratified:true and enforces nothing. Never invent one, and never reuse one from another run."
    },
    "timeoutMs": {
      "type": "integer",
      "maximum": 55000,
      "minimum": 1000,
      "description": "How long this call blocks, in milliseconds (up to 55000 is accepted; one call blocks at most 48 seconds)."
    },
    "allowedPaths": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "minLength": 1
      },
      "maxItems": 40,
      "description": "Globs you intend to change, e.g. [\"src/**\", \"docs/*.md\"]. File paths only: a word that is not a path (\"hubspot\", \"summer-campaign\") matches no file and makes every edit read as out of scope. A bare directory is expanded for you, so \"docs\" also covers \"docs/**\". Omit or leave empty to propose no path restriction, which the user is told plainly."
    },
    "offLimitsPaths": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "minLength": 1
      },
      "maxItems": 40,
      "description": "Globs you promise not to touch, e.g. [\".env*\", \"infra/**\"]. These win wherever they overlap allowedPaths. A leading \"**/\" needs a directory before it, so \"**/.env*\" is expanded for you to also cover a root \".env\"."
    }
  }
}
propose_scope output schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "correlationId",
    "ratified",
    "answered",
    "enforces",
    "contract"
  ],
  "properties": {
    "note": {
      "type": "string",
      "description": "Present only when the scope is not in force, saying what to do instead of proceeding."
    },
    "value": {
      "type": "string",
      "description": "The raw answer behind ratified, \"yes\" or \"no\"."
    },
    "status": {
      "enum": [
        "answered",
        "pending",
        "cancelled",
        "expired",
        "missing",
        "unavailable",
        "notified",
        "terminal",
        "stopped"
      ],
      "type": "string",
      "description": "The underlying scope-question state."
    },
    "answered": {
      "type": "boolean",
      "description": "True when the user responded at all. Answered but not ratified means they declined, so ask what scope they want rather than proceeding."
    },
    "contract": {
      "type": "object",
      "required": [
        "allowedPaths",
        "offLimitsPaths",
        "doneWhen"
      ],
      "properties": {
        "doneWhen": {
          "type": "string",
          "description": "What finished means for this run, as the user saw it."
        },
        "promises": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Non-path boundaries as the user saw them. Recorded, never enforced."
        },
        "allowedPaths": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Globs the run may change, after expansion. Empty means no path restriction was proposed, which the user was told plainly."
        },
        "offLimitsPaths": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Globs the run promised not to touch, after expansion. These win wherever they overlap allowedPaths."
        }
      },
      "description": "The scope as it is STORED and gated, echoed back so you and the server hold the same contract. Paths are the EXPANDED ones: a bare directory and a leading \"**/\" each gain the variant they would otherwise have missed, so this can contain more entries than you sent. The user was shown the paths you sent, because the added twin says the same thing to a reader; the expansion only changes what the matcher covers, never what it means.",
      "additionalProperties": {}
    },
    "enforces": {
      "type": "array",
      "items": {
        "enum": [
          "paths"
        ],
        "type": "string"
      },
      "description": "What this contract CONTAINS that can be checked, not a promise about what the gate on this machine will do. An EMPTY array means nothing here is checked automatically: the contract is a recorded promise, and every action stays governed by the permission policy exactly as it was before. Never tell the user a boundary is enforced when this is empty."
    },
    "hookSeen": {
      "type": "boolean",
      "description": "Whether any agent hook has actually reported this sessionId. FALSE means the gate will look this contract up under a key that does not exist, so nothing will be checked no matter what ratified says: fix the session id rather than proceeding as if a scope were in force. ABSENT means the check could not run, which is not evidence either way."
    },
    "ratified": {
      "type": "boolean",
      "description": "True only on an explicit yes. The contract is in force for this session only when this is true; anything else means proceed as if no scope was agreed."
    },
    "nextAction": {
      "enum": [
        "wait_for_answer",
        "ask_in_current_client"
      ],
      "type": "string",
      "description": "Keep waiting with the same correlationId while the proposal is pending. Follow handoffAction first when present; a poll ending does not expire or cancel the proposal."
    },
    "correlationId": {
      "type": "string",
      "description": "Id of the scope question. Keep waiting with this id while the question is pending."
    },
    "handoffAction": {
      "enum": [
        "cancel_then_ask_in_current_client",
        "stop"
      ],
      "type": "string",
      "description": "Race-safe directive for updated clients. Takes precedence over nextAction."
    },
    "enforcementNote": {
      "type": "string",
      "description": "Present only when there is something true to say about the limits of this contract. Repeat it to the user rather than paraphrasing it."
    }
  },
  "additionalProperties": {}
}
propose_scope annotations
{
  "title": "Propose Run Scope",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
wait_for_answer — Wait for User Answer
Advertised by the server during the last successful anonymous check.
Advertised read-only

Read the answer or current state of an existing Pushary question by its correlationId. Waits up to timeoutMs, capped at 48 seconds; a poll that ends with status pending is not an answer, so keep waiting. Returns answered, the answer when available, status and handoff information. Only pending is a live unanswered question; cancelled, expired, missing and unavailable are terminal states. Does not send another question.

wait_for_answer input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "correlationId"
  ],
  "properties": {
    "timeoutMs": {
      "type": "integer",
      "maximum": 55000,
      "minimum": 1000,
      "description": "How long this one poll blocks, in milliseconds (default 30000; up to 55000 is accepted, and one poll blocks at most 48 seconds). Follow handoffAction when present, otherwise nextAction."
    },
    "correlationId": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "The correlationId from an earlier ask_user response, or the linkedCorrelationId from a send_notification with an embedded askQuestion"
    }
  }
}
wait_for_answer output schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "answered",
    "status"
  ],
  "properties": {
    "hint": {
      "type": "string",
      "description": "What to do next, when there is a next step."
    },
    "note": {
      "type": "string",
      "description": "Free text the user added alongside their answer."
    },
    "value": {
      "type": "string",
      "description": "The user's answer: \"yes\" or \"no\" for confirm, the chosen option for select, the typed text for input. Present only when answered is true."
    },
    "status": {
      "enum": [
        "answered",
        "pending",
        "cancelled",
        "expired",
        "missing",
        "unavailable"
      ],
      "type": "string",
      "description": "The actual question state. Only pending is a live unanswered wait."
    },
    "answered": {
      "type": "boolean",
      "description": "True once the user responded. False means follow handoffAction when present, otherwise nextAction; do not guess that every unanswered state is a timeout."
    },
    "nextAction": {
      "enum": [
        "wait_for_answer",
        "ask_in_current_client"
      ],
      "type": "string",
      "description": "Backward-compatible next step for older clients. Follow handoffAction first when present."
    },
    "answerSource": {
      "enum": [
        "dashboard",
        "mobile_app",
        "mobile_background",
        "decide_page",
        "live_page",
        "answer_link",
        "inbox",
        "slack",
        "mac_app",
        "sandbox"
      ],
      "type": "string",
      "description": "Recorded answering surface, when known. Missing provenance is not proof of a phone answer; sandbox is simulated."
    },
    "handoffAction": {
      "enum": [
        "cancel_then_ask_in_current_client",
        "stop"
      ],
      "type": "string",
      "description": "Race-safe directive for updated clients. Takes precedence over nextAction."
    }
  },
  "additionalProperties": {}
}
wait_for_answer annotations
{
  "title": "Wait for User Answer",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
cancel_question — Cancel Pending Question
Advertised by the server during the last successful anonymous check.
Advertised destructive

Retract a pending question that is no longer needed or is moving to the current client. Returns cancelled:true only when a pending question was removed. False may mean it was already answered, expired, missing or unavailable. An unavailable state includes handoffAction:stop because cancellation could not safely inspect or fence the question. Does not retract an answer already recorded.

cancel_question input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "correlationId"
  ],
  "properties": {
    "handoff": {
      "type": "boolean",
      "description": "True when you are cancelling because you are about to ask the same question in the current client. For the next minute the Pushary hook then lets your own question tool through instead of sending it back to the phone. Set automatically whenever a live question is cancelled."
    },
    "correlationId": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "The correlationId of the pending question to cancel, as returned by ask_user or send_notification"
    }
  }
}
cancel_question output schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "cancelled",
    "correlationId"
  ],
  "properties": {
    "hint": {
      "type": "string",
      "description": "What to do when cancellation could not safely inspect the question."
    },
    "status": {
      "type": "string",
      "const": "unavailable",
      "description": "Present when Pushary could not safely read or fence the question state."
    },
    "cancelled": {
      "type": "boolean",
      "description": "True when a still-pending question was removed. False when it was already terminal, missing, or unavailable; inspect status and handoffAction when present."
    },
    "askHandoff": {
      "type": "boolean",
      "description": "True when the session was marked as handing off to the current client, so the hook will not re-ask on the phone for the next minute."
    },
    "correlationId": {
      "type": "string",
      "description": "The question this result refers to, echoed back."
    },
    "handoffAction": {
      "type": "string",
      "const": "stop",
      "description": "Stop rather than opening another answer surface when the question state is unavailable."
    },
    "cancelledByThisCall": {
      "type": "boolean",
      "description": "True only when this call moved the question from pending to cancelled. False when it was already cancelled, by an earlier call or another surface, or was never pending. Use it, not cancelled, to decide whether this call won a race to settle the question."
    }
  },
  "additionalProperties": {}
}
cancel_question annotations
{
  "title": "Cancel Pending Question",
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}

Prompts

Not advertised

No prompt definitions available.

Resources

5 advertised

site-index — Pushary site index (llms.txt)

Advertised by the server during the last successful anonymous check.

What Pushary is, what it is not, the questions it answers, and a linked map of every page on pushary.com. Start here.

site-index URI
https://pushary.com/llms.txt

MIME type: text/markdown

documentation — Pushary documentation (full corpus)

Advertised by the server during the last successful anonymous check.

Every Pushary documentation page concatenated into one Markdown file: quickstart, API keys, per-agent setup guides, permission policies, the audit trail, and the embed integration guide.

documentation URI
https://pushary.com/llms-full.txt

MIME type: text/markdown

openapi — Pushary REST API (OpenAPI 3.1)

Advertised by the server during the last successful anonymous check.

The full REST surface: decisions, authorization, end-user enrollment, per-end-user keys, notifications, subscribers, campaigns, templates and flows. Typed schemas and a unique operationId per operation.

openapi URI
https://pushary.com/openapi.json

MIME type: application/json

quickstart — Quickstart: connect an agent to Pushary

Advertised by the server during the last successful anonymous check.

The one-command setup that wires the MCP server, permission hooks and event tracking into an agent, and what to check when it does not connect.

quickstart URI
https://pushary.com/docs/agents/quickstart.md

MIME type: text/markdown

tool-reference — Pushary MCP tool reference

Advertised by the server during the last successful anonymous check.

What every tool on this server does, what it takes, and what it returns: ask_user, wait_for_answer, send_notification, cancel_question, and the Partner decision tools.

tool-reference URI
https://pushary.com/docs/agents/reference/tools.md

MIME type: text/markdown

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
10052 ms
95th percentile latency
10052 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 10052 ms · metadata changed · 6 tool(s), 5 resource(s), 0 resource template(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http pushary-mcp https://pushary.com/api/mcp/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add pushary-mcp --url https://pushary.com/api/mcp/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable pushary-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "pushary-mcp": {
      "url": "https://pushary.com/api/mcp/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "pushary-mcp": {
      "type": "streamableHttp",
      "url": "https://pushary.com/api/mcp/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start pushary-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "pushary-mcp": {
      "type": "http",
      "url": "https://pushary.com/api/mcp/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http pushary-mcp https://pushary.com/api/mcp/mcp

Claude Desktop

Open Claude Settings > Connectors > Add custom connector, enter this server URL and enable it for the conversation. This is a manual remote-connector handoff, not a desktop JSON configuration.
Claude Desktop configuration
https://pushary.com/api/mcp/mcp

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "pushary-mcp": {
      "serverUrl": "https://pushary.com/api/mcp/mcp"
    }
  }
}

Let your agent find its tools

Find servers like Pushary MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.