@planifyapps/mcp
Advanced tools
+25
-1
@@ -34,3 +34,3 @@ #!/usr/bin/env node | ||
| import { z } from 'zod'; | ||
| const VERSION = '0.1.0'; | ||
| const VERSION = '0.2.0'; | ||
| const DEFAULT_BASE = 'https://planifyapps.com/api/v1'; | ||
@@ -134,2 +134,14 @@ const API_KEY = process.env.PLANIFY_API_KEY ?? ''; | ||
| }); | ||
| /* ─────────────────────────────── Annotations ─────────────────────────────── */ | ||
| /** | ||
| * Every tool declares whether it can change anything. Clients use this to decide | ||
| * what to run without asking — and directory review requires it — but the reason | ||
| * to get it right is that "fetch the Pinterest boards" and "publish to a real | ||
| * audience" should never look alike to something deciding whether to confirm. | ||
| * | ||
| * openWorldHint is true throughout: every tool reaches a live external service. | ||
| */ | ||
| const READS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }; | ||
| /** Creates something, but nothing anyone can see and nothing that can't be redone. */ | ||
| const WRITES = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }; | ||
| server.registerTool('planify_me', { | ||
@@ -140,2 +152,3 @@ title: 'Planify: account and plan', | ||
| inputSchema: {}, | ||
| annotations: READS, | ||
| }, async () => text(await call('GET', '/me'))); | ||
@@ -148,2 +161,3 @@ server.registerTool('planify_list_channels', { | ||
| inputSchema: {}, | ||
| annotations: READS, | ||
| }, async () => text(await call('GET', '/channels'))); | ||
@@ -159,2 +173,3 @@ server.registerTool('planify_channel_settings', { | ||
| }, | ||
| annotations: READS, | ||
| }, async ({ channel_id }) => text(await call('GET', `/channels/${encodeURIComponent(channel_id)}/settings`))); | ||
@@ -172,2 +187,5 @@ server.registerTool('planify_channel_trigger', { | ||
| }, | ||
| // A POST on the wire, but a lookup in effect: it fetches boards and communities | ||
| // and changes nothing. Annotated for what it does, not for its HTTP verb. | ||
| annotations: READS, | ||
| }, async ({ channel_id, method_name, data }) => text(await call('POST', `/channels/${encodeURIComponent(channel_id)}/trigger`, { | ||
@@ -186,2 +204,3 @@ body: { method_name, data: data ?? {} }, | ||
| }, | ||
| annotations: READS, | ||
| }, async ({ channel_id, count, after }) => text(await call('GET', `/channels/${encodeURIComponent(channel_id)}/slot`, { | ||
@@ -198,2 +217,3 @@ query: { count: count ? String(count) : undefined, after }, | ||
| }, | ||
| annotations: WRITES, | ||
| }, async ({ urls }) => text(await call('POST', '/media/from-url', { body: { urls } }))); | ||
@@ -235,2 +255,6 @@ server.registerTool('planify_create_post', { | ||
| }, | ||
| // destructiveHint even though this only ever creates: with type "now" the result | ||
| // is public and irreversible, and the hint exists so a client knows to confirm. | ||
| // Annotating the common case (a draft) would hide the case that actually matters. | ||
| annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, | ||
| }, async (args) => { | ||
@@ -237,0 +261,0 @@ const { idempotency_key, type, ...rest } = args; |
+3
-1
| { | ||
| "name": "@planifyapps/mcp", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Model Context Protocol server for Planify — let an AI assistant schedule and publish social posts.", | ||
| "//": "mcpName is how the MCP Registry proves this npm package belongs to the server it lists. It must stay identical to `name` in server.json.", | ||
| "mcpName": "com.planifyapps/planify", | ||
| "keywords": [ | ||
@@ -6,0 +8,0 @@ "mcp", |
+22
-0
@@ -74,2 +74,24 @@ # @planifyapps/mcp | ||
| ## Releasing | ||
| Two places have to move together. The registry lists metadata only — it fetches the | ||
| npm package and checks that its `package.json` carries an `mcpName` matching the | ||
| `name` in `server.json`. Publish to the registry before npm and it fails validation. | ||
| Bump the version in three places (`package.json`, `VERSION` in `src/index.ts`, and | ||
| both `version` fields in `server.json`), then: | ||
| ```bash | ||
| npm publish --access public # npm first, always | ||
| mcp-publisher login http --domain planifyapps.com \ | ||
| --private-key "$(openssl pkey -in .mcp-registry-key.pem -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')" | ||
| mcp-publisher publish | ||
| ``` | ||
| Domain authentication is what lets the server be named `com.planifyapps/planify` | ||
| rather than sitting under a personal GitHub namespace. The proof is the public key | ||
| at `/.well-known/mcp-registry-auth`, served from `public/` in the web app; the | ||
| matching private key is `.mcp-registry-key.pem`, gitignored and kept out of the | ||
| repo. Lose it and you regenerate the pair and redeploy the proof file. | ||
| ## Links | ||
@@ -76,0 +98,0 @@ |
22474
14.49%305
8.54%104
26.83%