atproto-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +5 -6
  2. package/dist/cli.d.ts +8 -1
  3. package/dist/cli.js +25 -7
  4. package/dist/health-check.d.ts +0 -1
  5. package/dist/health-check.js +2 -2
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.js +142 -64
  8. package/dist/prompts/index.d.ts +12 -4
  9. package/dist/prompts/index.js +21 -11
  10. package/dist/resources/base.d.ts +0 -1
  11. package/dist/resources/base.js +0 -1
  12. package/dist/resources/conversation-context-resource.d.ts +7 -8
  13. package/dist/resources/conversation-context-resource.js +10 -12
  14. package/dist/resources/index.d.ts +1 -3
  15. package/dist/resources/index.js +6 -6
  16. package/dist/tools/implementations/advanced-social-tools.d.ts +0 -1
  17. package/dist/tools/implementations/advanced-social-tools.js +13 -2
  18. package/dist/tools/implementations/analytics-tools.d.ts +13 -2
  19. package/dist/tools/implementations/analytics-tools.js +12 -8
  20. package/dist/tools/implementations/analyze-account-tool.d.ts +452 -1
  21. package/dist/tools/implementations/analyze-account-tool.js +397 -9
  22. package/dist/tools/implementations/base-tool.d.ts +49 -7
  23. package/dist/tools/implementations/base-tool.js +49 -14
  24. package/dist/tools/implementations/batch-operations-tools.d.ts +22 -8
  25. package/dist/tools/implementations/batch-operations-tools.js +131 -28
  26. package/dist/tools/implementations/composite-tools.d.ts +0 -1
  27. package/dist/tools/implementations/composite-tools.js +0 -1
  28. package/dist/tools/implementations/content-discovery-tools.d.ts +8 -3
  29. package/dist/tools/implementations/content-discovery-tools.js +100 -67
  30. package/dist/tools/implementations/content-management-tools.d.ts +91 -9
  31. package/dist/tools/implementations/content-management-tools.js +51 -32
  32. package/dist/tools/implementations/create-post-tool.d.ts +399 -11
  33. package/dist/tools/implementations/create-post-tool.js +116 -33
  34. package/dist/tools/implementations/create-thread-tool.d.ts +1 -2
  35. package/dist/tools/implementations/create-thread-tool.js +17 -1
  36. package/dist/tools/implementations/discover-tool.d.ts +235 -1
  37. package/dist/tools/implementations/discover-tool.js +238 -21
  38. package/dist/tools/implementations/follow-user-tool.d.ts +0 -1
  39. package/dist/tools/implementations/follow-user-tool.js +0 -1
  40. package/dist/tools/implementations/get-author-feed-tool.d.ts +0 -1
  41. package/dist/tools/implementations/get-author-feed-tool.js +0 -1
  42. package/dist/tools/implementations/get-user-profile-tool.d.ts +0 -1
  43. package/dist/tools/implementations/get-user-profile-tool.js +0 -1
  44. package/dist/tools/implementations/index.d.ts +0 -1
  45. package/dist/tools/implementations/index.js +0 -1
  46. package/dist/tools/implementations/like-post-tool.d.ts +9 -3
  47. package/dist/tools/implementations/like-post-tool.js +15 -6
  48. package/dist/tools/implementations/media-tools.d.ts +90 -8
  49. package/dist/tools/implementations/media-tools.js +292 -60
  50. package/dist/tools/implementations/moderation-tools.d.ts +0 -1
  51. package/dist/tools/implementations/moderation-tools.js +0 -1
  52. package/dist/tools/implementations/reply-to-post-tool.d.ts +5 -2
  53. package/dist/tools/implementations/reply-to-post-tool.js +15 -2
  54. package/dist/tools/implementations/repost-tool.d.ts +3 -2
  55. package/dist/tools/implementations/repost-tool.js +5 -13
  56. package/dist/tools/implementations/rich-media-tools.d.ts +13 -15
  57. package/dist/tools/implementations/rich-media-tools.js +2 -17
  58. package/dist/tools/implementations/search-actors-tool.d.ts +0 -1
  59. package/dist/tools/implementations/search-actors-tool.js +0 -1
  60. package/dist/tools/implementations/search-posts-tool.d.ts +3 -32
  61. package/dist/tools/implementations/search-posts-tool.js +0 -60
  62. package/dist/tools/implementations/social-graph-tools.d.ts +1 -3
  63. package/dist/tools/implementations/social-graph-tools.js +5 -3
  64. package/dist/tools/implementations/timeline-tools.d.ts +0 -28
  65. package/dist/tools/implementations/timeline-tools.js +0 -74
  66. package/dist/tools/index.d.ts +0 -1
  67. package/dist/tools/index.js +0 -1
  68. package/dist/types/index.d.ts +30 -12
  69. package/dist/types/index.js +6 -3
  70. package/dist/utils/atp-client.d.ts +15 -2
  71. package/dist/utils/atp-client.js +64 -11
  72. package/dist/utils/config.d.ts +3 -2
  73. package/dist/utils/config.js +10 -7
  74. package/dist/utils/firehose-client.d.ts +0 -1
  75. package/dist/utils/firehose-client.js +0 -1
  76. package/dist/utils/logger.d.ts +8 -1
  77. package/dist/utils/logger.js +17 -4
  78. package/dist/utils/oauth-client.d.ts +0 -1
  79. package/dist/utils/oauth-client.js +4 -5
  80. package/dist/utils/performance.d.ts +0 -1
  81. package/dist/utils/performance.js +0 -1
  82. package/dist/utils/security.d.ts +0 -1
  83. package/dist/utils/security.js +0 -1
  84. package/dist/utils/url-safety.d.ts +0 -1
  85. package/dist/utils/url-safety.js +0 -1
  86. package/package.json +11 -6
  87. package/dist/cli.d.ts.map +0 -1
  88. package/dist/cli.js.map +0 -1
  89. package/dist/health-check.d.ts.map +0 -1
  90. package/dist/health-check.js.map +0 -1
  91. package/dist/index.d.ts.map +0 -1
  92. package/dist/index.js.map +0 -1
  93. package/dist/prompts/index.d.ts.map +0 -1
  94. package/dist/prompts/index.js.map +0 -1
  95. package/dist/resources/base.d.ts.map +0 -1
  96. package/dist/resources/base.js.map +0 -1
  97. package/dist/resources/conversation-context-resource.d.ts.map +0 -1
  98. package/dist/resources/conversation-context-resource.js.map +0 -1
  99. package/dist/resources/index.d.ts.map +0 -1
  100. package/dist/resources/index.js.map +0 -1
  101. package/dist/test/integration-config.d.ts +0 -60
  102. package/dist/test/integration-config.d.ts.map +0 -1
  103. package/dist/test/integration-config.js +0 -93
  104. package/dist/test/integration-config.js.map +0 -1
  105. package/dist/test/setup.d.ts +0 -78
  106. package/dist/test/setup.d.ts.map +0 -1
  107. package/dist/test/setup.js +0 -138
  108. package/dist/test/setup.js.map +0 -1
  109. package/dist/tools/implementations/advanced-social-tools.d.ts.map +0 -1
  110. package/dist/tools/implementations/advanced-social-tools.js.map +0 -1
  111. package/dist/tools/implementations/analytics-tools.d.ts.map +0 -1
  112. package/dist/tools/implementations/analytics-tools.js.map +0 -1
  113. package/dist/tools/implementations/analyze-account-tool.d.ts.map +0 -1
  114. package/dist/tools/implementations/analyze-account-tool.js.map +0 -1
  115. package/dist/tools/implementations/base-tool.d.ts.map +0 -1
  116. package/dist/tools/implementations/base-tool.js.map +0 -1
  117. package/dist/tools/implementations/batch-operations-tools.d.ts.map +0 -1
  118. package/dist/tools/implementations/batch-operations-tools.js.map +0 -1
  119. package/dist/tools/implementations/composite-tools.d.ts.map +0 -1
  120. package/dist/tools/implementations/composite-tools.js.map +0 -1
  121. package/dist/tools/implementations/content-discovery-tools.d.ts.map +0 -1
  122. package/dist/tools/implementations/content-discovery-tools.js.map +0 -1
  123. package/dist/tools/implementations/content-management-tools.d.ts.map +0 -1
  124. package/dist/tools/implementations/content-management-tools.js.map +0 -1
  125. package/dist/tools/implementations/create-post-tool.d.ts.map +0 -1
  126. package/dist/tools/implementations/create-post-tool.js.map +0 -1
  127. package/dist/tools/implementations/create-thread-tool.d.ts.map +0 -1
  128. package/dist/tools/implementations/create-thread-tool.js.map +0 -1
  129. package/dist/tools/implementations/discover-tool.d.ts.map +0 -1
  130. package/dist/tools/implementations/discover-tool.js.map +0 -1
  131. package/dist/tools/implementations/follow-user-tool.d.ts.map +0 -1
  132. package/dist/tools/implementations/follow-user-tool.js.map +0 -1
  133. package/dist/tools/implementations/get-author-feed-tool.d.ts.map +0 -1
  134. package/dist/tools/implementations/get-author-feed-tool.js.map +0 -1
  135. package/dist/tools/implementations/get-user-profile-tool.d.ts.map +0 -1
  136. package/dist/tools/implementations/get-user-profile-tool.js.map +0 -1
  137. package/dist/tools/implementations/index.d.ts.map +0 -1
  138. package/dist/tools/implementations/index.js.map +0 -1
  139. package/dist/tools/implementations/like-post-tool.d.ts.map +0 -1
  140. package/dist/tools/implementations/like-post-tool.js.map +0 -1
  141. package/dist/tools/implementations/media-tools.d.ts.map +0 -1
  142. package/dist/tools/implementations/media-tools.js.map +0 -1
  143. package/dist/tools/implementations/moderation-tools.d.ts.map +0 -1
  144. package/dist/tools/implementations/moderation-tools.js.map +0 -1
  145. package/dist/tools/implementations/reply-to-post-tool.d.ts.map +0 -1
  146. package/dist/tools/implementations/reply-to-post-tool.js.map +0 -1
  147. package/dist/tools/implementations/repost-tool.d.ts.map +0 -1
  148. package/dist/tools/implementations/repost-tool.js.map +0 -1
  149. package/dist/tools/implementations/rich-media-tools.d.ts.map +0 -1
  150. package/dist/tools/implementations/rich-media-tools.js.map +0 -1
  151. package/dist/tools/implementations/search-actors-tool.d.ts.map +0 -1
  152. package/dist/tools/implementations/search-actors-tool.js.map +0 -1
  153. package/dist/tools/implementations/search-posts-tool.d.ts.map +0 -1
  154. package/dist/tools/implementations/search-posts-tool.js.map +0 -1
  155. package/dist/tools/implementations/social-graph-tools.d.ts.map +0 -1
  156. package/dist/tools/implementations/social-graph-tools.js.map +0 -1
  157. package/dist/tools/implementations/timeline-tools.d.ts.map +0 -1
  158. package/dist/tools/implementations/timeline-tools.js.map +0 -1
  159. package/dist/tools/index.d.ts.map +0 -1
  160. package/dist/tools/index.js.map +0 -1
  161. package/dist/types/index.d.ts.map +0 -1
  162. package/dist/types/index.js.map +0 -1
  163. package/dist/utils/atp-client.d.ts.map +0 -1
  164. package/dist/utils/atp-client.js.map +0 -1
  165. package/dist/utils/config.d.ts.map +0 -1
  166. package/dist/utils/config.js.map +0 -1
  167. package/dist/utils/firehose-client.d.ts.map +0 -1
  168. package/dist/utils/firehose-client.js.map +0 -1
  169. package/dist/utils/logger.d.ts.map +0 -1
  170. package/dist/utils/logger.js.map +0 -1
  171. package/dist/utils/oauth-client.d.ts.map +0 -1
  172. package/dist/utils/oauth-client.js.map +0 -1
  173. package/dist/utils/performance.d.ts.map +0 -1
  174. package/dist/utils/performance.js.map +0 -1
  175. package/dist/utils/security.d.ts.map +0 -1
  176. package/dist/utils/security.js.map +0 -1
  177. package/dist/utils/url-safety.d.ts.map +0 -1
  178. package/dist/utils/url-safety.js.map +0 -1
package/README.md CHANGED
@@ -43,8 +43,7 @@ write operations, private data, feeds).
43
43
  > public-data mode — no credentials required.
44
44
  >
45
45
  > **Recent additions**: Batch operations for bulk actions, advanced analytics
46
- > and insights, intelligent content discovery, and a conversation-context
47
- > scratchpad resource.
46
+ > and insights, and intelligent content discovery.
48
47
 
49
48
  ## Architecture
50
49
 
@@ -227,10 +226,10 @@ config:
227
226
  `get_timeline`)
228
227
  - All write operations (create, like, repost, follow, etc.)
229
228
  - Resources (timeline, profile, notifications) - these are listed but require
230
- authentication to return data (the `conversation-context` scratchpad resource
231
- is readable without auth and simply returns near-empty placeholder content)
232
- - Prompts (content composition, reply templates) - these are listed but require
233
- authentication to be available
229
+ authentication to return data
230
+
231
+ Prompts (content composition, reply templates) are pure text templates and work
232
+ without authentication.
234
233
 
235
234
  **Important:** All tools, resources, and prompts are listed by the MCP server
236
235
  regardless of authentication state. Most tools and resources that require
package/dist/cli.d.ts CHANGED
@@ -6,5 +6,12 @@
6
6
  * Main CLI function
7
7
  */
8
8
  declare function main(): Promise<void>;
9
+ /**
10
+ * Detect whether a module is the process entry point. Node realpath-resolves
11
+ * import.meta.url for the main module, but argv[1] stays the literal invoked
12
+ * path — and npm installs bins as symlinks — so a naive string comparison
13
+ * against `file://${argv[1]}` breaks symlinked, relative, and space-containing
14
+ * paths. Compare realpath-resolved file URLs instead.
15
+ */
16
+ export declare function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean;
9
17
  export { main as runCli };
10
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Command-line interface for the AT Protocol MCP Server
4
4
  */
5
5
  import { parseArgs } from 'node:util';
6
- import { existsSync, readFileSync } from 'node:fs';
6
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { ConfigurationError } from './types/index.js';
10
10
  import { AtpMcpServer } from './index.js';
11
11
  import { LogLevel, Logger } from './utils/logger.js';
@@ -73,7 +73,7 @@ const CLI_OPTIONS = {
73
73
  },
74
74
  host: {
75
75
  type: 'string',
76
- short: 'h',
76
+ short: 'H',
77
77
  description: 'Server host (default: localhost)',
78
78
  },
79
79
  service: {
@@ -93,6 +93,7 @@ const CLI_OPTIONS = {
93
93
  },
94
94
  help: {
95
95
  type: 'boolean',
96
+ short: 'h',
96
97
  description: 'Show help message',
97
98
  },
98
99
  version: {
@@ -118,11 +119,11 @@ currently have no effect.
118
119
 
119
120
  Options:
120
121
  -p, --port <number> Server port (reserved; stdio transport ignores it)
121
- -h, --host <string> Server host (reserved; stdio transport ignores it)
122
+ -H, --host <string> Server host (reserved; stdio transport ignores it)
122
123
  -s, --service <url> AT Protocol service URL (default: https://bsky.social)
123
124
  -a, --auth <method> Authentication method: app-password|oauth (optional)
124
125
  -l, --log-level <level> Log level: debug|info|warn|error (default: info)
125
- --help Show this help message
126
+ -h, --help Show this help message
126
127
  -v, --version Show version information
127
128
 
128
129
  🔓 Unauthenticated Mode (Default):
@@ -311,12 +312,29 @@ async function main() {
311
312
  process.exit(1);
312
313
  }
313
314
  }
315
+ /**
316
+ * Detect whether a module is the process entry point. Node realpath-resolves
317
+ * import.meta.url for the main module, but argv[1] stays the literal invoked
318
+ * path — and npm installs bins as symlinks — so a naive string comparison
319
+ * against `file://${argv[1]}` breaks symlinked, relative, and space-containing
320
+ * paths. Compare realpath-resolved file URLs instead.
321
+ */
322
+ export function isMainModule(importMetaUrl, argv1) {
323
+ if (argv1 == null || argv1 === '') {
324
+ return false;
325
+ }
326
+ try {
327
+ return importMetaUrl === pathToFileURL(realpathSync(argv1)).href;
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
314
333
  // Run CLI if this file is executed directly
315
- if (import.meta.url === `file://${process.argv[1]}`) {
334
+ if (isMainModule(import.meta.url, process.argv[1])) {
316
335
  main().catch(error => {
317
336
  console.error('Fatal error:', error);
318
337
  process.exit(1);
319
338
  });
320
339
  }
321
340
  export { main as runCli };
322
- //# sourceMappingURL=cli.js.map
@@ -11,4 +11,3 @@
11
11
  * and reporting them would be misleading.
12
12
  */
13
13
  export {};
14
- //# sourceMappingURL=health-check.d.ts.map
@@ -10,6 +10,7 @@
10
10
  * connection counts of the running server — a fresh process cannot observe those
11
11
  * and reporting them would be misleading.
12
12
  */
13
+ import { isMainModule } from './cli.js';
13
14
  import { AtpMcpServer } from './index.js';
14
15
  function healthCheck() {
15
16
  try {
@@ -47,7 +48,6 @@ function healthCheck() {
47
48
  }
48
49
  }
49
50
  // Run health check if this script is executed directly
50
- if (import.meta.url === `file://${process.argv[1]}`) {
51
+ if (isMainModule(import.meta.url, process.argv[1])) {
51
52
  void healthCheck();
52
53
  }
53
- //# sourceMappingURL=health-check.js.map
package/dist/index.d.ts CHANGED
@@ -131,4 +131,3 @@ export declare class AtpMcpServer {
131
131
  * await server.start();
132
132
  */
133
133
  export default AtpMcpServer;
134
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
11
+ import { ErrorCode, ListResourceTemplatesRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
12
12
  import { z } from 'zod';
13
13
  import { zodToJsonSchema } from 'zod-to-json-schema';
14
14
  import { ConfigurationError, ValidationError } from './types/index.js';
@@ -21,57 +21,108 @@ import { createPrompts } from './prompts/index.js';
21
21
  import { PerformanceMonitor } from './utils/performance.js';
22
22
  import { SecurityManager } from './utils/security.js';
23
23
  /**
24
- * Pure read tools (no writes to the network). readOnlyHint:true lets clients
25
- * auto-approve them. Curated explicitly per tool NOT derived from auth mode,
26
- * which encodes auth requirement, not destructiveness.
24
+ * MCP spec error code for "Resource not found" (resources/read with an unknown
25
+ * URI). Not part of the SDK's ErrorCode enum, which only covers the generic
26
+ * JSON-RPC codes.
27
27
  */
28
- const READ_ONLY_TOOLS = new Set([
29
- 'analyze_account',
30
- 'analyze_image',
31
- 'analyze_moderation_status',
32
- 'discover',
33
- 'discover_communities',
34
- 'find_influential_users',
35
- 'find_similar_users',
36
- 'generate_link_preview',
37
- 'get_author_feed',
38
- 'get_custom_feed',
39
- 'get_list',
40
- 'get_notifications',
41
- 'get_post_context',
42
- 'get_timeline',
43
- 'get_user_connections',
44
- 'get_user_profile',
45
- 'get_user_summary',
46
- 'search_actors',
47
- 'search_posts',
48
- ]);
28
+ const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
49
29
  /**
50
- * Tools whose effect is irreversible or removes/limits data (deletes, blocks,
51
- * mutes, reports, removals, token revocation). destructiveHint:true lets clients
52
- * surface confirmation UI and withhold auto-approval.
30
+ * Explicit per-tool annotation hints, advertised verbatim in tools/list.
31
+ *
32
+ * Per MCP spec defaults, clients assume the worst case for any hint a server
33
+ * omits on a non-read-only tool (destructiveHint: true, idempotentHint:
34
+ * false), so every write tool carries both hints explicitly.
35
+ *
36
+ * - readOnlyHint: pure reads (no writes to the network). Curated explicitly
37
+ * per tool — NOT derived from auth mode, which encodes auth requirement,
38
+ * not destructiveness. destructive/idempotent hints are only meaningful for
39
+ * write tools and are omitted on read-only entries.
40
+ * - destructiveHint: the tool may delete or overwrite existing data/state
41
+ * (record deletes, list removals, profile overwrites, blocks, irreversible
42
+ * moderation reports). Purely additive, reversible writes carry an explicit
43
+ * false.
44
+ * - idempotentHint: repeating the call with identical arguments has no
45
+ * additional effect. Claimed only where verified — an implementation-level
46
+ * dedup/no-op path, or set/clear semantics of the underlying XRPC endpoint.
53
47
  */
54
- const DESTRUCTIVE_TOOLS = new Set([
55
- 'block_user',
56
- 'delete_post',
57
- 'mute_user',
58
- 'remove_from_list',
59
- 'report_content',
60
- 'report_user',
61
- 'unfollow_user',
62
- 'unlike_post',
63
- 'unrepost',
64
- ]);
48
+ const TOOL_ANNOTATIONS = {
49
+ // Pure read tools.
50
+ analyze_account: { readOnlyHint: true },
51
+ analyze_image: { readOnlyHint: true },
52
+ analyze_moderation_status: { readOnlyHint: true },
53
+ discover: { readOnlyHint: true },
54
+ discover_communities: { readOnlyHint: true },
55
+ find_influential_users: { readOnlyHint: true },
56
+ find_similar_users: { readOnlyHint: true },
57
+ generate_link_preview: { readOnlyHint: true },
58
+ get_author_feed: { readOnlyHint: true },
59
+ get_custom_feed: { readOnlyHint: true },
60
+ get_list: { readOnlyHint: true },
61
+ get_notifications: { readOnlyHint: true },
62
+ get_post_context: { readOnlyHint: true },
63
+ get_timeline: { readOnlyHint: true },
64
+ get_user_connections: { readOnlyHint: true },
65
+ get_user_profile: { readOnlyHint: true },
66
+ get_user_summary: { readOnlyHint: true },
67
+ search_actors: { readOnlyHint: true },
68
+ search_posts: { readOnlyHint: true },
69
+ // Additive, reversible writes. Idempotency notes name the verified
70
+ // dedup/no-op path in the implementation.
71
+ // add_to_list has no dedup: duplicate listitem records are possible.
72
+ add_to_list: { destructiveHint: false, idempotentHint: false },
73
+ // batch_action only supports follow/like/repost (no quote text), and every
74
+ // path dedups per target via authoritative viewer state.
75
+ batch_action: { destructiveHint: false, idempotentHint: true },
76
+ create_list: { destructiveHint: false, idempotentHint: false },
77
+ create_post: { destructiveHint: false, idempotentHint: false },
78
+ create_thread: { destructiveHint: false, idempotentHint: false },
79
+ // Dedups via viewer.following.
80
+ follow_user: { destructiveHint: false, idempotentHint: true },
81
+ // Dedups via viewer.like.
82
+ like_post: { destructiveHint: false, idempotentHint: true },
83
+ // seenAt defaults to "now", so repeated calls advance the seen marker.
84
+ mark_notifications_seen: { destructiveHint: false, idempotentHint: false },
85
+ // app.bsky.graph.muteActor sets server-side state; repeating it is a no-op.
86
+ mute_user: { destructiveHint: false, idempotentHint: true },
87
+ reply_to_post: { destructiveHint: false, idempotentHint: false },
88
+ // Plain reposts dedup via viewer.repost, but quote text creates a new post
89
+ // on every call, so the tool as a whole is not idempotent.
90
+ repost: { destructiveHint: false, idempotentHint: false },
91
+ upload_image: { destructiveHint: false, idempotentHint: false },
92
+ upload_video: { destructiveHint: false, idempotentHint: false },
93
+ // Destructive writes: may delete or overwrite data/state. These hints let
94
+ // clients surface confirmation UI and withhold auto-approval.
95
+ // Blocking imposes hard bidirectional restrictions and has no dedup
96
+ // (duplicate block records are possible).
97
+ block_user: { destructiveHint: true, idempotentHint: false },
98
+ delete_post: { destructiveHint: true, idempotentHint: false },
99
+ // "Not in list" resolves to an explicit success:false no-op.
100
+ remove_from_list: { destructiveHint: true, idempotentHint: true },
101
+ // Reports are irreversible moderation actions against third parties; each
102
+ // call files a new report.
103
+ report_content: { destructiveHint: true, idempotentHint: false },
104
+ report_user: { destructiveHint: true, idempotentHint: false },
105
+ // "Not blocked" resolves to an explicit success:false no-op.
106
+ unblock_user: { destructiveHint: true, idempotentHint: true },
107
+ unfollow_user: { destructiveHint: true, idempotentHint: false },
108
+ unlike_post: { destructiveHint: true, idempotentHint: false },
109
+ // app.bsky.graph.unmuteActor clears server-side state; repeating is a no-op.
110
+ unmute_user: { destructiveHint: true, idempotentHint: true },
111
+ unrepost: { destructiveHint: true, idempotentHint: false },
112
+ // Overwrites profile fields; the read-merge-write (CAS-guarded) converges
113
+ // to the same record for identical arguments.
114
+ update_profile: { destructiveHint: true, idempotentHint: true },
115
+ };
65
116
  /**
66
117
  * Build the advertised annotations for a tool. openWorldHint is true for every
67
118
  * tool (they all reach a live network). A per-tool `schema.annotations` override
68
- * wins over these defaults.
119
+ * wins over these defaults. Tools missing from TOOL_ANNOTATIONS fall back to
120
+ * the MCP client-side worst-case defaults (destructive, non-idempotent).
69
121
  */
70
122
  function computeToolAnnotations(method, override) {
71
123
  return {
72
124
  openWorldHint: true,
73
- ...(READ_ONLY_TOOLS.has(method) ? { readOnlyHint: true } : {}),
74
- ...(DESTRUCTIVE_TOOLS.has(method) ? { destructiveHint: true } : {}),
125
+ ...(TOOL_ANNOTATIONS[method] ?? {}),
75
126
  ...(override ?? {}),
76
127
  };
77
128
  }
@@ -176,10 +227,12 @@ export class AtpMcpServer {
176
227
  inputSchema: tool.schema.params
177
228
  ? this.zodToJsonSchema(tool.schema.params)
178
229
  : { type: 'object', properties: {} },
179
- // Advertise an output schema when the tool declares one. This is purely
180
- // descriptive metadata in the tools/list payload; because tools/call
181
- // responses are built by this custom handler (not the SDK's high-level
182
- // registerTool), it does not trigger structuredContent validation.
230
+ // Advertise the tool's declared output schema. This is a binding
231
+ // contract, not decoration: spec-compliant clients (including the
232
+ // official SDK's Client.callTool) validate every tools/call
233
+ // structuredContent against this schema and hard-fail on mismatch,
234
+ // so each outputSchema literal must track its tool's actual return
235
+ // shape (see the structuredContent note in the tools/call handler).
183
236
  ...(tool.schema.outputSchema ? { outputSchema: tool.schema.outputSchema } : {}),
184
237
  annotations: computeToolAnnotations(tool.schema.method, tool.schema.annotations),
185
238
  })),
@@ -251,10 +304,13 @@ export class AtpMcpServer {
251
304
  // to re-parse the pretty-printed string. SDK structuredContent must be a
252
305
  // JSON object, so bare arrays/primitives are wrapped under `result`.
253
306
  //
254
- // NOTE: we intentionally do NOT declare per-tool `outputSchema` doing so
255
- // makes the SDK REQUIRE and validate structuredContent on every call and
256
- // throw on any non-conforming object. structuredContent here is additive
257
- // and best-effort.
307
+ // NOTE: per-tool `outputSchema` IS advertised in tools/list (see
308
+ // registerTools above), and SDK clients validate structuredContent
309
+ // against it, failing the call on any drift. structuredContent is
310
+ // therefore a contract, not best-effort: tools that declare an
311
+ // outputSchema must return a conforming object. (Server-side, this
312
+ // custom handler performs no validation of its own — the SDK only
313
+ // auto-validates when tools are registered via registerTool.)
258
314
  const structuredContent = result != null && typeof result === 'object' && !Array.isArray(result)
259
315
  ? result
260
316
  : { result };
@@ -311,6 +367,13 @@ export class AtpMcpServer {
311
367
  mimeType: resource.mimeType,
312
368
  })),
313
369
  }));
370
+ // Register resources/templates/list handler. The declared resources
371
+ // capability invites clients to probe this method; without a handler the
372
+ // SDK answers -32601 Method not found. No URI templates are offered (all
373
+ // resources have fixed URIs), so the list is empty.
374
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
375
+ resourceTemplates: [],
376
+ }));
314
377
  // Register resources/read handler
315
378
  this.server.setRequestHandler(z.object({
316
379
  method: z.literal('resources/read'),
@@ -321,7 +384,9 @@ export class AtpMcpServer {
321
384
  try {
322
385
  const resource = resources.find(r => r.uri === request.params.uri);
323
386
  if (!resource) {
324
- throw new McpError(ErrorCode.InvalidParams, `Resource not found: ${request.params.uri}`, {
387
+ // The MCP spec reserves -32002 for "Resource not found"; the SDK's
388
+ // ErrorCode enum does not (yet) name it, so use the literal.
389
+ throw new McpError(RESOURCE_NOT_FOUND_ERROR_CODE, `Resource not found: ${request.params.uri}`, {
325
390
  uri: request.params.uri,
326
391
  });
327
392
  }
@@ -331,13 +396,22 @@ export class AtpMcpServer {
331
396
  throw new McpError(ErrorCode.InternalError, `Resource not available: ${request.params.uri}`, { uri: request.params.uri });
332
397
  }
333
398
  const content = await resource.read();
399
+ // Per the MCP resource content schema, each item is either text
400
+ // contents or base64 blob contents. Binary payloads must be passed
401
+ // through as a blob, not coerced to empty text.
334
402
  return {
335
403
  contents: [
336
- {
337
- uri: content.uri,
338
- mimeType: content.mimeType,
339
- text: content.text ?? '',
340
- },
404
+ content.blob
405
+ ? {
406
+ uri: content.uri,
407
+ mimeType: content.mimeType,
408
+ blob: Buffer.from(content.blob).toString('base64'),
409
+ }
410
+ : {
411
+ uri: content.uri,
412
+ mimeType: content.mimeType,
413
+ text: content.text ?? '',
414
+ },
341
415
  ],
342
416
  };
343
417
  }
@@ -376,11 +450,10 @@ export class AtpMcpServer {
376
450
  name: request.params.name,
377
451
  });
378
452
  }
379
- // Check if prompt is available
380
- const isAvailable = prompt.isAvailable();
381
- if (!isAvailable) {
382
- throw new McpError(ErrorCode.InternalError, `Prompt not available: ${request.params.name}`, { name: request.params.name });
383
- }
453
+ // No availability/auth gate here: prompts are pure text templates
454
+ // that never touch the AT Protocol client. Required arguments are
455
+ // enforced inside prompt.get(), which throws an InvalidParams
456
+ // McpError that toHandlerMcpError passes through verbatim.
384
457
  const messages = await prompt.get(request.params.arguments ?? {});
385
458
  return { messages };
386
459
  }
@@ -465,8 +538,14 @@ export class AtpMcpServer {
465
538
  }
466
539
  catch (error) {
467
540
  this.logger.error('Failed to start AT Protocol MCP Server', error);
468
- // Cleanup on failure
469
- await this.cleanup();
541
+ // Cleanup on failure — but never let a failing cleanup mask the
542
+ // original startup error, which is the one the caller must see.
543
+ try {
544
+ await this.cleanup();
545
+ }
546
+ catch (cleanupError) {
547
+ this.logger.error('Cleanup after failed startup also failed', cleanupError);
548
+ }
470
549
  if (error instanceof ConfigurationError) {
471
550
  throw error;
472
551
  }
@@ -600,4 +679,3 @@ export class AtpMcpServer {
600
679
  * await server.start();
601
680
  */
602
681
  export default AtpMcpServer;
603
- //# sourceMappingURL=index.js.map
@@ -34,9 +34,18 @@ export declare abstract class BasePrompt implements IMcpPrompt {
34
34
  protected logger: Logger;
35
35
  constructor(atpClient: AtpClient, loggerName: string);
36
36
  /**
37
- * Check if the prompt is available
37
+ * Check if the prompt is available.
38
+ *
39
+ * Prompts are pure text templates: they never call the AT Protocol client,
40
+ * so they are available regardless of authentication state.
38
41
  */
39
42
  isAvailable(): boolean;
43
+ /**
44
+ * Read a declared-required string argument, rejecting missing/blank values
45
+ * with the spec invalid-params error instead of silently substituting a
46
+ * placeholder.
47
+ */
48
+ protected requireStringArg(args: Record<string, unknown> | undefined, name: string): string;
40
49
  /**
41
50
  * Generate the prompt content
42
51
  */
@@ -47,7 +56,7 @@ export declare abstract class BasePrompt implements IMcpPrompt {
47
56
  */
48
57
  export declare class ContentCompositionPrompt extends BasePrompt {
49
58
  readonly name = "content_composition";
50
- readonly description = "Generate engaging social media post content with proper formatting and hashtags. Requires authentication.";
59
+ readonly description = "Generate engaging social media post content with proper formatting and hashtags. Pure text template; works without authentication.";
51
60
  readonly arguments: {
52
61
  name: string;
53
62
  description: string;
@@ -61,7 +70,7 @@ export declare class ContentCompositionPrompt extends BasePrompt {
61
70
  */
62
71
  export declare class ReplyTemplatePrompt extends BasePrompt {
63
72
  readonly name = "reply_template";
64
- readonly description = "Generate thoughtful reply templates for different types of posts. Requires authentication.";
73
+ readonly description = "Generate thoughtful reply templates for different types of posts. Pure text template; works without authentication.";
65
74
  readonly arguments: {
66
75
  name: string;
67
76
  description: string;
@@ -74,4 +83,3 @@ export declare class ReplyTemplatePrompt extends BasePrompt {
74
83
  * Create all MCP prompts for AT Protocol content assistance
75
84
  */
76
85
  export declare function createPrompts(atpClient: AtpClient): BasePrompt[];
77
- //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * MCP Prompts for AT Protocol content creation assistance
3
3
  */
4
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
4
5
  import { Logger } from '../utils/logger.js';
5
6
  /**
6
7
  * Base class for MCP prompts
@@ -13,15 +14,25 @@ export class BasePrompt {
13
14
  this.logger = new Logger(loggerName);
14
15
  }
15
16
  /**
16
- * Check if the prompt is available
17
+ * Check if the prompt is available.
18
+ *
19
+ * Prompts are pure text templates: they never call the AT Protocol client,
20
+ * so they are available regardless of authentication state.
17
21
  */
18
22
  isAvailable() {
19
- try {
20
- return this.atpClient.isAuthenticated();
21
- }
22
- catch {
23
- return false;
23
+ return true;
24
+ }
25
+ /**
26
+ * Read a declared-required string argument, rejecting missing/blank values
27
+ * with the spec invalid-params error instead of silently substituting a
28
+ * placeholder.
29
+ */
30
+ requireStringArg(args, name) {
31
+ const value = args?.[name];
32
+ if (typeof value !== 'string' || value.trim() === '') {
33
+ throw new McpError(ErrorCode.InvalidParams, `Missing required argument "${name}" for prompt "${this.name}"`, { prompt: this.name, argument: name });
24
34
  }
35
+ return value;
25
36
  }
26
37
  }
27
38
  /**
@@ -29,7 +40,7 @@ export class BasePrompt {
29
40
  */
30
41
  export class ContentCompositionPrompt extends BasePrompt {
31
42
  name = 'content_composition';
32
- description = 'Generate engaging social media post content with proper formatting and hashtags. Requires authentication.';
43
+ description = 'Generate engaging social media post content with proper formatting and hashtags. Pure text template; works without authentication.';
33
44
  arguments = [
34
45
  {
35
46
  name: 'topic',
@@ -56,7 +67,7 @@ export class ContentCompositionPrompt extends BasePrompt {
56
67
  super(atpClient, 'ContentCompositionPrompt');
57
68
  }
58
69
  async get(args) {
59
- const topic = args?.['topic'] ?? 'general topic';
70
+ const topic = this.requireStringArg(args, 'topic');
60
71
  const tone = args?.['tone'] ?? 'casual';
61
72
  const length = args?.['length'] ?? 'medium';
62
73
  const includeHashtags = args?.['include_hashtags'] !== false;
@@ -104,7 +115,7 @@ Please provide the post text ready to publish.`,
104
115
  */
105
116
  export class ReplyTemplatePrompt extends BasePrompt {
106
117
  name = 'reply_template';
107
- description = 'Generate thoughtful reply templates for different types of posts. Requires authentication.';
118
+ description = 'Generate thoughtful reply templates for different types of posts. Pure text template; works without authentication.';
108
119
  arguments = [
109
120
  {
110
121
  name: 'original_post',
@@ -126,7 +137,7 @@ export class ReplyTemplatePrompt extends BasePrompt {
126
137
  super(atpClient, 'ReplyTemplatePrompt');
127
138
  }
128
139
  async get(args) {
129
- const originalPost = args?.['original_post'] ?? 'the original post';
140
+ const originalPost = this.requireStringArg(args, 'original_post');
130
141
  const replyType = args?.['reply_type'] ?? 'supportive';
131
142
  const relationship = args?.['relationship'] ?? 'stranger';
132
143
  const replyTypeGuidance = {
@@ -191,4 +202,3 @@ export function createPrompts(atpClient) {
191
202
  logger.info(`Created ${prompts.length} AT Protocol MCP prompts`);
192
203
  return prompts;
193
204
  }
194
- //# sourceMappingURL=index.js.map
@@ -35,4 +35,3 @@ export declare abstract class BaseResource implements IMcpResource {
35
35
  */
36
36
  isAvailable(): Promise<boolean>;
37
37
  }
38
- //# sourceMappingURL=base.d.ts.map
@@ -24,4 +24,3 @@ export class BaseResource {
24
24
  }
25
25
  }
26
26
  }
27
- //# sourceMappingURL=base.js.map
@@ -41,14 +41,14 @@ interface IConversationContext {
41
41
  /**
42
42
  * Conversation Context Resource
43
43
  *
44
- * This resource maintains state across LLM interactions, tracking:
45
- * - Posts that have been discussed in the conversation
46
- * - Active threads being followed
47
- * - Users that have been mentioned
48
- * - Search queries performed
49
- * - Recent actions taken
44
+ * In-memory store for conversation state (discussed posts, active threads,
45
+ * mentioned users, searches, recent actions), exposed as an MCP resource.
50
46
  *
51
- * This helps LLMs maintain context and provide more coherent, contextual responses.
47
+ * NOT REGISTERED with the MCP server (see createResources): MCP has no
48
+ * client-write mechanism for resources, and no tool currently calls the
49
+ * static add* methods, so the resource would always read as empty. The class
50
+ * is retained so a future change can populate it from tool handlers and
51
+ * re-register it.
52
52
  */
53
53
  export declare class ConversationContextResource extends BaseResource {
54
54
  readonly uri = "atproto://conversation-context";
@@ -110,4 +110,3 @@ export declare class ConversationContextResource extends BaseResource {
110
110
  static getContext(): IConversationContext;
111
111
  }
112
112
  export {};
113
- //# sourceMappingURL=conversation-context-resource.d.ts.map
@@ -5,22 +5,21 @@ import { BaseResource } from './base.js';
5
5
  /**
6
6
  * Conversation Context Resource
7
7
  *
8
- * This resource maintains state across LLM interactions, tracking:
9
- * - Posts that have been discussed in the conversation
10
- * - Active threads being followed
11
- * - Users that have been mentioned
12
- * - Search queries performed
13
- * - Recent actions taken
8
+ * In-memory store for conversation state (discussed posts, active threads,
9
+ * mentioned users, searches, recent actions), exposed as an MCP resource.
14
10
  *
15
- * This helps LLMs maintain context and provide more coherent, contextual responses.
11
+ * NOT REGISTERED with the MCP server (see createResources): MCP has no
12
+ * client-write mechanism for resources, and no tool currently calls the
13
+ * static add* methods, so the resource would always read as empty. The class
14
+ * is retained so a future change can populate it from tool handlers and
15
+ * re-register it.
16
16
  */
17
17
  export class ConversationContextResource extends BaseResource {
18
18
  uri = 'atproto://conversation-context';
19
19
  name = 'Conversation Context';
20
- description = 'Scratchpad for conversation state (recently discussed posts, active threads, mentioned ' +
21
- 'users, recent actions). NOTE: the server does not yet populate this automatically during ' +
22
- 'tool calls, so it is empty unless a client explicitly writes to it; treat empty arrays as ' +
23
- '"not tracked", not "nothing happened".';
20
+ description = 'Server-side scratchpad of conversation state (recently discussed posts, active threads, ' +
21
+ 'mentioned users, recent actions). Populated only by server-side tool integrations; in the ' +
22
+ 'current release nothing writes to it, so it always reads as empty arrays.';
24
23
  mimeType = 'application/json';
25
24
  static context = {
26
25
  recentlyDiscussedPosts: [],
@@ -177,4 +176,3 @@ export class ConversationContextResource extends BaseResource {
177
176
  return this.context;
178
177
  }
179
178
  }
180
- //# sourceMappingURL=conversation-context-resource.js.map
@@ -37,10 +37,8 @@ export declare class NotificationsResource extends BaseResource {
37
37
  constructor(atpClient: AtpClient);
38
38
  read(): Promise<IResourceContent>;
39
39
  }
40
- import { ConversationContextResource } from './conversation-context-resource.js';
41
- export { ConversationContextResource };
40
+ export { ConversationContextResource } from './conversation-context-resource.js';
42
41
  /**
43
42
  * Create all MCP resources for AT Protocol data
44
43
  */
45
44
  export declare function createResources(atpClient: AtpClient): BaseResource[];
46
- //# sourceMappingURL=index.d.ts.map