{"_id":"@365loopa/365m","name":"@365loopa/365m","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@365loopa/365m","version":"0.1.0","description":"CLI and MCP server for the 365Marketing agent action catalog — one tool per action your credential may invoke.","license":"SEE LICENSE IN LICENSE","type":"module","engines":{"node":">=20"},"bin":{"365m":"bin/365m.mjs"},"exports":{".":"./src/mcp-core.mjs","./mcp-core":"./src/mcp-core.mjs","./catalog":"./src/catalog.mjs","./package.json":"./package.json"},"keywords":["mcp","modelcontextprotocol","cli","agent","365loopa"],"homepage":"https://app.365loopa.com","repository":{"type":"git","url":"git+https://github.com/laojin1900/365marketing.git","directory":"packages/365m"},"publishConfig":{"access":"public"},"_id":"@365loopa/365m@0.1.0","bugs":{"url":"https://github.com/laojin1900/365marketing/issues"},"_nodeVersion":"26.0.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-AbFOChg2Q6Q7kAreGXpUXRiqXzkFf53OdtSAvkHsM2oWut+0PpVbDFDmEaLTBwFYaVAys3O0mk/3oiSpO0IZig==","shasum":"ccc6e760bc7fd7c49895cbffa0df871671da0a88","tarball":"https://registry.npmjs.org/@365loopa/365m/-/365m-0.1.0.tgz","fileCount":9,"unpackedSize":46340,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDxBP+FcnR02kgTahfoqik/ovEM1uEdEDr2oOWOSSTFvAIgM5I/nRRfpG6fgFUPHxcNB6e3hzI+qjQjApJIFWEi9Mo="}]},"_npmUser":{"name":"365loopa","email":"baiduboy@gmail.com"},"directories":{},"maintainers":[{"name":"365loopa","email":"baiduboy@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/365m_0.1.0_1788406164526_0.733621233246518"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-03T03:29:24.338Z","0.1.0":"2026-09-03T03:29:24.707Z","modified":"2026-09-03T03:29:24.928Z"},"maintainers":[{"name":"365loopa","email":"baiduboy@gmail.com"}],"description":"CLI and MCP server for the 365Marketing agent action catalog — one tool per action your credential may invoke.","homepage":"https://app.365loopa.com","keywords":["mcp","modelcontextprotocol","cli","agent","365loopa"],"repository":{"type":"git","url":"git+https://github.com/laojin1900/365marketing.git","directory":"packages/365m"},"bugs":{"url":"https://github.com/laojin1900/365marketing/issues"},"license":"SEE LICENSE IN LICENSE","readme":"# `@365loopa/365m`\n\nThe CLI and MCP server for the [365Loopa](https://app.365loopa.com) agent action catalog.\n\nEvery action your credential may invoke — reading the paid-growth workspace, pausing an exact\nad, approving a publish job — is exposed twice: as a shell command, and as an MCP tool. Both\nsend the same authenticated HTTP request to the same routes a human session uses. There is no\nsecond execution path and no separate \"agent permission model\": **an agent has exactly the\npermissions of the user it acts for**, read live from that user's membership on every request.\n\nZero runtime dependencies. Node 20 or newer.\n\n## Get a credential\n\nCreate an agent credential in the console under **agent credentials**. The plaintext is shown\nexactly once — store it in a secret manager, not in shell history.\n\n```bash\nexport M365_AGENT_TOKEN=365m_at_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n`M365_ORIGIN` defaults to `https://app.365loopa.com`; set it only for another deployment.\n\n## Command line\n\n```bash\nnpx -y @365loopa/365m catalog        # what this credential can do\nnpx -y @365loopa/365m whoami         # who it acts for\nnpx -y @365loopa/365m call <actionId> [flags]\n```\n\nInstall it properly if you use it often: `npm i -g @365loopa/365m`, then just `365m`.\n\n### `365m catalog [--json]`\n\n`GET /api/agent/catalog`. A human table (id, method + path, tier, roles,\nreadOnly/destructive) by default; `--json` prints the raw response document, which is also\nwhat you should build tooling against.\n\n### `365m whoami`\n\nPrints the catalog's `principal`: user id, organization id, role, and how you authenticated.\n\n### `365m call <actionId> [flags]`\n\nLooks the action up in the **live** catalog — not a hard-coded route table — then invokes it.\n\n| Flag | Repeatable | Purpose |\n| --- | --- | --- |\n| `--param k=v` | yes | Fills a `{k}` placeholder in the action's route path. Every placeholder must be supplied or the call is refused before anything is sent. |\n| `--query k=v` | yes | Appended to the query string. |\n| `--body k=v` | yes | Sets a JSON body field, with light coercion: `true`/`false`/`null`/a bare number are typed, anything else is a string. Use `k:=<json>` to pass a raw JSON value instead. |\n| `--json '<object>'` | no | Replaces the whole body with this parsed JSON object. `--body` is ignored when this is given. |\n| `--request-id <uuid>` | no | Sets `x-request-id`. A UUID is generated when omitted — the header is always sent. |\n| `--idempotency-key <key>` | no | Sets `idempotency-key`. Only sent when the action's `idempotency` is `HEADER_REQUIRED`; a key is generated when one is needed and none is given. |\n| `--dry-run` | no | Prints the request (method, url, headers with the credential redacted, body) and exits 0 without sending it. |\n| `--origin <url>` | no | Overrides `M365_ORIGIN` for one invocation. |\n\n```bash\n# A read, scoped to one account.\n365m call paid.workspace.read --query account=ad_account_0000000000000000000000ab\n\n# A write, previewed first. Nothing is sent; no spend is touched.\n365m call paid.meta.ad.pause_exact --dry-run \\\n  --body adGroupName=\"WAS - Broad\" \\\n  --body adName=\"Static 12 - v3\" \\\n  --body reason=\"CPA over ceiling for 3 consecutive days, see roi-scan run rs_2026-09-01\"\n```\n\nDrop `--dry-run` to send it. That action carries `idempotency: HEADER_REQUIRED`, so `365m`\nattaches a generated `idempotency-key` automatically; pass `--idempotency-key` yourself to\nmake a retry provably the same call.\n\n### Output and exit codes\n\nstdout carries exactly one thing: a human table for `catalog` without `--json`, otherwise\nexactly one JSON document — the response body on success, the response body plus `status` on\nan HTTP/business error, or `{ error: { code: \"USAGE\", message } }` on a usage error.\nDiagnostics never go to stdout. The credential is never printed, including by `--dry-run`.\n\n| Exit code | Meaning |\n| --- | --- |\n| `0` | Success |\n| `1` | HTTP/business error (a non-2xx response reached the CLI) |\n| `2` | Usage error (bad flags, missing credential, unknown action, …) |\n\n## MCP\n\nThere are two ways to connect an MCP client, and they expose the identical tool surface —\nthey are two transports over one projection, not two implementations.\n\n### Remote (nothing to install)\n\nPoint the client at the deployment's own endpoint and pass your credential as a header. This\nis the better option when your client supports HTTP transports.\n\n```\nhttps://app.365loopa.com/api/agent/mcp\n```\n\nIt is stateless: each request is a complete JSON-RPC message, there is no session to resume,\nand there is no server-initiated stream (`GET` answers 405 and says so). Authentication is a\nstatic bearer credential — this endpoint does **not** implement MCP's OAuth discovery, so a\nclient that can only authenticate by OAuth cannot use it; use the stdio server below instead.\n\n### Local, over stdio\n\nFor clients that only speak stdio, or when you would rather the credential live in your own\nenvironment than in a client config file:\n\n```bash\nnpx -y @365loopa/365m mcp\n```\n\nIt speaks MCP on stdin/stdout and reads `M365_AGENT_TOKEN` and `M365_ORIGIN` from the\nenvironment. On launch it fetches the catalog and builds its tool list from the response; a\nmissing credential, a 401/403, or a network error prints one line to stderr and exits\nnon-zero. Nothing but protocol frames ever reaches stdout, so a failed start cannot corrupt\nthe client's stream — and the server never starts with an empty tool list, which an agent\nwould misread as \"you are allowed to do nothing\".\n\n### What the tools look like\n\nTool `name` is the action's `id` with `.` replaced by `_` (`paid.meta.ad.pause_exact` →\n`paid_meta_ad_pause_exact`). `description` is the action's catalog description with one line\nappended, so an agent choosing between tools can see the stakes in the tool list itself:\n\n```\nTier T2 · stops spend · roles OWNER, OPS_ADMIN\n```\n\n`inputSchema` is `{ type: \"object\", additionalProperties: false, properties: { params?, query?,\nbody? } }`, built from the action's own `params`/`query`/`body` JSON Schemas — nothing is\ninvented or renamed. `annotations` restate `readOnlyHint`, `destructiveHint`,\n`idempotentHint`, and always `openWorldHint: false` (every call targets your deployment, never\nan arbitrary external host).\n\nRead the `365m://catalog` resource (`application/json`) to see every field — including the\nsub-schemas — without spending a tool call.\n\n### Errors\n\nA successful response comes back as `content: [{ type: \"text\", text: <the response JSON> }]`.\nA non-2xx response comes back the same way with `isError: true`, and the envelope's\n`error.code` is preserved verbatim in that text. **Branch on `code`** — not on the `isError`\nflag alone, and never on `message`.\n\n### T3 actions are two-phase, on purpose\n\nNo tool here ever moves money on its own call. An action tagged\n`approval: { kind: \"TWO_PHASE\" }` in the catalog is split into two tools: the `propose` tool\nreturns a frozen manifest and nothing executes; a human approves that manifest out of band\n(a chat card or the console — never through MCP); only then does the `execute` tool, given the\napproval, actually run. If you are driving this as an agent and a proposal is sitting\nunapproved, surface it — do not look for a way to skip the second phase.\n\n## License\n\nProprietary; see [LICENSE](./LICENSE). Use of this client is permitted for accessing the\n365Loopa service.\n","readmeFilename":"README.md","_rev":"1-4b218b2d8218394820dcfe6c4c66de52"}