posterly-mcp-server 0.44.0 → 0.45.1
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.
- package/README.md +310 -111
- package/dist/index.js +190 -894
- package/dist/lib/alias-transport.d.ts +84 -0
- package/dist/lib/alias-transport.js +193 -0
- package/dist/lib/api-client.d.ts +18 -0
- package/dist/lib/api-client.js +55 -0
- package/dist/lib/preview-token-core.d.ts +62 -0
- package/dist/lib/preview-token-core.js +131 -0
- package/dist/lib/preview-token.d.ts +23 -0
- package/dist/lib/preview-token.js +44 -0
- package/dist/lib/tool-registry.d.ts +246 -0
- package/dist/lib/tool-registry.js +394 -0
- package/dist/lib/tool-runtime.d.ts +68 -0
- package/dist/lib/tool-runtime.js +85 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/audit-google-business-profile.js +1 -1
- package/dist/tools/connect-account.d.ts +4 -4
- package/dist/tools/connect-account.js +2 -2
- package/dist/tools/create-connect-session.d.ts +2 -2
- package/dist/tools/create-connect-session.js +2 -2
- package/dist/tools/create-post.d.ts +30 -7
- package/dist/tools/create-post.js +75 -9
- package/dist/tools/create-posts-batch.d.ts +4 -4
- package/dist/tools/create-posts-batch.js +14 -2
- package/dist/tools/create-webhook.d.ts +2 -2
- package/dist/tools/delete-post.d.ts +34 -10
- package/dist/tools/delete-post.js +52 -16
- package/dist/tools/generate-captions.d.ts +2 -2
- package/dist/tools/generate-image.d.ts +4 -4
- package/dist/tools/generate-image.js +2 -2
- package/dist/tools/generate-video.d.ts +6 -6
- package/dist/tools/generate-video.js +1 -1
- package/dist/tools/get-agent-signup-info.js +2 -2
- package/dist/tools/get-post.d.ts +16 -1
- package/dist/tools/get-post.js +35 -19
- package/dist/tools/list-accounts.d.ts +49 -3
- package/dist/tools/list-accounts.js +148 -33
- package/dist/tools/list-analytics.d.ts +111 -0
- package/dist/tools/list-analytics.js +393 -0
- package/dist/tools/list-brands.d.ts +38 -3
- package/dist/tools/list-brands.js +138 -20
- package/dist/tools/list-comments.d.ts +29 -9
- package/dist/tools/list-comments.js +75 -34
- package/dist/tools/list-conversations.d.ts +32 -9
- package/dist/tools/list-conversations.js +63 -27
- package/dist/tools/list-google-business-media.js +1 -1
- package/dist/tools/list-google-business-reviews.d.ts +32 -9
- package/dist/tools/list-google-business-reviews.js +77 -52
- package/dist/tools/list-jobs.d.ts +60 -0
- package/dist/tools/list-jobs.js +81 -0
- package/dist/tools/list-platforms.d.ts +62 -3
- package/dist/tools/list-platforms.js +118 -9
- package/dist/tools/list-posts.d.ts +2 -2
- package/dist/tools/manage-comment.d.ts +102 -0
- package/dist/tools/manage-comment.js +125 -0
- package/dist/tools/manage-conversation.d.ts +79 -0
- package/dist/tools/manage-conversation.js +75 -0
- package/dist/tools/manage-google-business-media.d.ts +103 -0
- package/dist/tools/manage-google-business-media.js +126 -0
- package/dist/tools/manage-google-business-review.d.ts +146 -0
- package/dist/tools/manage-google-business-review.js +115 -0
- package/dist/tools/manage-oauth-client.d.ts +121 -0
- package/dist/tools/manage-oauth-client.js +94 -0
- package/dist/tools/manage-webhook.d.ts +99 -0
- package/dist/tools/manage-webhook.js +87 -0
- package/dist/tools/start-signup.d.ts +2 -2
- package/dist/tools/submit-agent-feedback.d.ts +4 -4
- package/dist/tools/trigger-platform-helper.d.ts +2 -2
- package/dist/tools/update-post.d.ts +62 -11
- package/dist/tools/update-post.js +79 -26
- package/dist/tools/upload-media.d.ts +32 -9
- package/dist/tools/upload-media.js +54 -18
- package/dist/tools/validate-post.d.ts +4 -4
- package/dist/tools/validate-post.js +2 -2
- package/dist/tools/whoami.d.ts +15 -2
- package/dist/tools/whoami.js +221 -25
- package/package.json +5 -3
- package/scripts/test-alias-transport.mjs +368 -0
- package/scripts/test-client-header-sanitization.mjs +78 -0
- package/scripts/test-payload-builders.mjs +143 -20
- package/scripts/test-tool-annotations.mjs +38 -17
- package/server.json +9 -2
- package/src/index.ts +227 -1443
- package/src/lib/alias-transport.ts +233 -0
- package/src/lib/api-client.ts +65 -0
- package/src/lib/preview-token-core.ts +157 -0
- package/src/lib/preview-token.ts +54 -0
- package/src/lib/tool-registry.ts +538 -0
- package/src/lib/tool-runtime.ts +117 -0
- package/src/lib/version.ts +1 -1
- package/src/tools/audit-google-business-profile.ts +1 -1
- package/src/tools/connect-account.ts +2 -2
- package/src/tools/create-connect-session.ts +2 -2
- package/src/tools/create-post.ts +87 -12
- package/src/tools/create-posts-batch.ts +13 -2
- package/src/tools/delete-post.ts +60 -19
- package/src/tools/generate-image.ts +2 -2
- package/src/tools/generate-video.ts +1 -1
- package/src/tools/get-agent-signup-info.ts +2 -2
- package/src/tools/get-post.ts +35 -21
- package/src/tools/list-accounts.ts +187 -39
- package/src/tools/list-analytics.ts +444 -0
- package/src/tools/list-brands.ts +162 -26
- package/src/tools/list-comments.ts +86 -43
- package/src/tools/list-conversations.ts +81 -39
- package/src/tools/list-google-business-media.ts +1 -1
- package/src/tools/list-google-business-reviews.ts +93 -64
- package/src/tools/list-jobs.ts +91 -0
- package/src/tools/list-platforms.ts +135 -11
- package/src/tools/manage-comment.ts +142 -0
- package/src/tools/manage-conversation.ts +89 -0
- package/src/tools/manage-google-business-media.ts +147 -0
- package/src/tools/manage-google-business-review.ts +139 -0
- package/src/tools/manage-oauth-client.ts +116 -0
- package/src/tools/manage-webhook.ts +103 -0
- package/src/tools/update-post.ts +106 -39
- package/src/tools/upload-media.ts +76 -28
- package/src/tools/validate-post.ts +2 -1
- package/src/tools/whoami.ts +253 -30
- package/tool-annotations.json +76 -149
- package/tool-registry.json +2085 -0
- package/dist/tools/add-google-business-media.d.ts +0 -42
- package/dist/tools/add-google-business-media.js +0 -34
- package/dist/tools/create-oauth-client.d.ts +0 -40
- package/dist/tools/create-oauth-client.js +0 -20
- package/dist/tools/delete-comment.d.ts +0 -24
- package/dist/tools/delete-comment.js +0 -22
- package/dist/tools/delete-google-business-media.d.ts +0 -32
- package/dist/tools/delete-google-business-media.js +0 -17
- package/dist/tools/delete-google-business-review-reply.d.ts +0 -32
- package/dist/tools/delete-google-business-review-reply.js +0 -17
- package/dist/tools/delete-oauth-client.d.ts +0 -20
- package/dist/tools/delete-oauth-client.js +0 -13
- package/dist/tools/delete-post-group.d.ts +0 -20
- package/dist/tools/delete-post-group.js +0 -13
- package/dist/tools/delete-webhook.d.ts +0 -20
- package/dist/tools/delete-webhook.js +0 -13
- package/dist/tools/get-account-analytics.d.ts +0 -31
- package/dist/tools/get-account-analytics.js +0 -152
- package/dist/tools/get-brand-profile.d.ts +0 -16
- package/dist/tools/get-brand-profile.js +0 -73
- package/dist/tools/get-brand.d.ts +0 -16
- package/dist/tools/get-brand.js +0 -26
- package/dist/tools/get-comment.d.ts +0 -20
- package/dist/tools/get-comment.js +0 -36
- package/dist/tools/get-connect-link.d.ts +0 -28
- package/dist/tools/get-connect-link.js +0 -51
- package/dist/tools/get-connect-session.d.ts +0 -20
- package/dist/tools/get-connect-session.js +0 -54
- package/dist/tools/get-conversation.d.ts +0 -24
- package/dist/tools/get-conversation.js +0 -31
- package/dist/tools/get-google-business-review-link.d.ts +0 -24
- package/dist/tools/get-google-business-review-link.js +0 -19
- package/dist/tools/get-image-job.d.ts +0 -24
- package/dist/tools/get-image-job.js +0 -31
- package/dist/tools/get-learned-voice.d.ts +0 -16
- package/dist/tools/get-learned-voice.js +0 -22
- package/dist/tools/get-mcp-status.d.ts +0 -8
- package/dist/tools/get-mcp-status.js +0 -182
- package/dist/tools/get-performance-profile.d.ts +0 -16
- package/dist/tools/get-performance-profile.js +0 -39
- package/dist/tools/get-platform-schema.d.ts +0 -24
- package/dist/tools/get-platform-schema.js +0 -48
- package/dist/tools/get-post-analytics.d.ts +0 -39
- package/dist/tools/get-post-analytics.js +0 -222
- package/dist/tools/get-post-insights.d.ts +0 -28
- package/dist/tools/get-post-insights.js +0 -39
- package/dist/tools/get-post-missing.d.ts +0 -16
- package/dist/tools/get-post-missing.js +0 -12
- package/dist/tools/get-video-job.d.ts +0 -24
- package/dist/tools/get-video-job.js +0 -31
- package/dist/tools/list-brand-accounts.d.ts +0 -16
- package/dist/tools/list-brand-accounts.js +0 -24
- package/dist/tools/reply-google-business-review.d.ts +0 -36
- package/dist/tools/reply-google-business-review.js +0 -18
- package/dist/tools/reply-to-comment.d.ts +0 -32
- package/dist/tools/reply-to-comment.js +0 -17
- package/dist/tools/send-message.d.ts +0 -28
- package/dist/tools/send-message.js +0 -16
- package/dist/tools/suggest-google-business-review-reply.d.ts +0 -48
- package/dist/tools/suggest-google-business-review-reply.js +0 -27
- package/dist/tools/sync-inbox.d.ts +0 -24
- package/dist/tools/sync-inbox.js +0 -18
- package/dist/tools/test-webhook.d.ts +0 -20
- package/dist/tools/test-webhook.js +0 -14
- package/dist/tools/update-comment.d.ts +0 -28
- package/dist/tools/update-comment.js +0 -21
- package/dist/tools/update-oauth-client.d.ts +0 -44
- package/dist/tools/update-oauth-client.js +0 -21
- package/dist/tools/update-post-release-id.d.ts +0 -28
- package/dist/tools/update-post-release-id.js +0 -18
- package/dist/tools/update-post-status.d.ts +0 -28
- package/dist/tools/update-post-status.js +0 -23
- package/dist/tools/update-webhook.d.ts +0 -40
- package/dist/tools/update-webhook.js +0 -21
- package/dist/tools/upload-media-from-url.d.ts +0 -24
- package/dist/tools/upload-media-from-url.js +0 -26
- package/src/tools/add-google-business-media.ts +0 -50
- package/src/tools/create-oauth-client.ts +0 -36
- package/src/tools/delete-comment.ts +0 -29
- package/src/tools/delete-google-business-media.ts +0 -30
- package/src/tools/delete-google-business-review-reply.ts +0 -30
- package/src/tools/delete-oauth-client.ts +0 -23
- package/src/tools/delete-post-group.ts +0 -17
- package/src/tools/delete-webhook.ts +0 -17
- package/src/tools/get-account-analytics.ts +0 -171
- package/src/tools/get-brand-profile.ts +0 -86
- package/src/tools/get-brand.ts +0 -34
- package/src/tools/get-comment.ts +0 -47
- package/src/tools/get-connect-link.ts +0 -61
- package/src/tools/get-connect-session.ts +0 -63
- package/src/tools/get-conversation.ts +0 -44
- package/src/tools/get-google-business-review-link.ts +0 -30
- package/src/tools/get-image-job.ts +0 -36
- package/src/tools/get-learned-voice.ts +0 -31
- package/src/tools/get-mcp-status.ts +0 -212
- package/src/tools/get-performance-profile.ts +0 -47
- package/src/tools/get-platform-schema.ts +0 -56
- package/src/tools/get-post-analytics.ts +0 -243
- package/src/tools/get-post-insights.ts +0 -60
- package/src/tools/get-post-missing.ts +0 -16
- package/src/tools/get-video-job.ts +0 -36
- package/src/tools/list-brand-accounts.ts +0 -36
- package/src/tools/reply-google-business-review.ts +0 -32
- package/src/tools/reply-to-comment.ts +0 -30
- package/src/tools/send-message.ts +0 -28
- package/src/tools/suggest-google-business-review-reply.ts +0 -44
- package/src/tools/sync-inbox.ts +0 -28
- package/src/tools/test-webhook.ts +0 -18
- package/src/tools/update-comment.ts +0 -31
- package/src/tools/update-oauth-client.ts +0 -38
- package/src/tools/update-post-release-id.ts +0 -25
- package/src/tools/update-post-status.ts +0 -31
- package/src/tools/update-webhook.ts +0 -36
- package/src/tools/upload-media-from-url.ts +0 -33
package/dist/tools/whoami.d.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { type ToolContext } from '../lib/tool-runtime.js';
|
|
3
4
|
export declare const whoamiTool: {
|
|
4
5
|
name: string;
|
|
5
6
|
description: string;
|
|
6
|
-
inputSchema: z.ZodObject<{
|
|
7
|
-
|
|
7
|
+
inputSchema: z.ZodObject<{
|
|
8
|
+
view: z.ZodOptional<z.ZodEnum<["identity", "status"]>>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
view?: "status" | "identity" | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
view?: "status" | "identity" | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
15
|
+
legacySchemas: {
|
|
16
|
+
get_mcp_status: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
17
|
+
};
|
|
18
|
+
execute(client: PosterlyClient, input: {
|
|
19
|
+
view?: "identity" | "status";
|
|
20
|
+
}, ctx: ToolContext): Promise<string>;
|
|
8
21
|
};
|
package/dist/tools/whoami.js
CHANGED
|
@@ -1,31 +1,227 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { POSTERLY_MCP_VERSION } from '../lib/version.js';
|
|
3
|
+
import { resolvePosterlyBaseUrl } from '../lib/api-client.js';
|
|
2
4
|
import { code, mdKeyValue, mdTable, mdTitle } from '../lib/format.js';
|
|
5
|
+
import { validateArgs } from '../lib/tool-runtime.js';
|
|
6
|
+
const NPM_LATEST_URL = 'https://registry.npmjs.org/posterly-mcp-server/latest';
|
|
3
7
|
export const whoamiTool = {
|
|
4
8
|
name: 'whoami',
|
|
5
|
-
description: '
|
|
6
|
-
inputSchema: z.object({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
mdTable(['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'], info.workspaces.map((workspace) => [
|
|
22
|
-
`${workspace.name}${workspace.is_personal ? ' (personal)' : ''}`,
|
|
23
|
-
code(workspace.id),
|
|
24
|
-
workspace.role || 'member',
|
|
25
|
-
workspace.timezone || 'n/a',
|
|
26
|
-
workspace.id === info.default_workspace.id,
|
|
27
|
-
])),
|
|
28
|
-
'**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
|
|
29
|
-
].filter(Boolean).join('\n\n');
|
|
9
|
+
description: 'Who you are and how posterly is connected. view=identity (default): the authenticated user, API key scopes, workspaces, and onboarding next step. ALWAYS call this at the start of a session; if onboarding.completed is false, walk the remaining onboarding steps IN THIS conversation (about you, connect, brand voice, first post) and do not send the user to /onboarding-v2 or the dashboard composer wizard. view=status: MCP server version, latest npm version, endpoint and API key health, and update guidance; works before POSTERLY_API_KEY is configured.',
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
view: z
|
|
12
|
+
.enum(['identity', 'status'])
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('identity (default): user, scopes, workspaces, onboarding. status: MCP server and connection health.'),
|
|
15
|
+
}),
|
|
16
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
17
|
+
legacySchemas: {
|
|
18
|
+
get_mcp_status: z.object({}),
|
|
19
|
+
},
|
|
20
|
+
async execute(client, input, ctx) {
|
|
21
|
+
const { mode } = validateArgs(ctx, 'whoami', input);
|
|
22
|
+
if (mode === 'status')
|
|
23
|
+
return mcpStatus(client);
|
|
24
|
+
return identity(client, ctx);
|
|
30
25
|
},
|
|
31
26
|
};
|
|
27
|
+
async function identity(client, ctx) {
|
|
28
|
+
const info = await client.whoami();
|
|
29
|
+
const text = [
|
|
30
|
+
mdTitle('posterly identity'),
|
|
31
|
+
mdKeyValue([
|
|
32
|
+
['User', info.user.email || code(info.user.id)],
|
|
33
|
+
['API scopes', info.api_key.scopes.join(', ') || 'none'],
|
|
34
|
+
['Default workspace', `${info.default_workspace.name} (${code(info.default_workspace.id)})`],
|
|
35
|
+
['Default timezone', info.default_workspace.timezone || 'n/a'],
|
|
36
|
+
['Onboarding', info.onboarding?.completed ? 'complete' : info.onboarding?.next_step_label || 'unknown'],
|
|
37
|
+
]),
|
|
38
|
+
info.onboarding && !info.onboarding.completed
|
|
39
|
+
? `**Onboarding next:** ${info.onboarding.next_step_label}. ${info.onboarding.instruction}`
|
|
40
|
+
: '',
|
|
41
|
+
mdTable(['Workspace', 'Workspace ID', 'Role', 'Timezone', 'Default'], info.workspaces.map((workspace) => [
|
|
42
|
+
`${workspace.name}${workspace.is_personal ? ' (personal)' : ''}`,
|
|
43
|
+
code(workspace.id),
|
|
44
|
+
workspace.role || 'member',
|
|
45
|
+
workspace.timezone || 'n/a',
|
|
46
|
+
workspace.id === info.default_workspace.id,
|
|
47
|
+
])),
|
|
48
|
+
'**Tip:** Pass `workspace_id` to `create_post`, `list_posts`, `list_accounts`, and `find_available_slot` when you want the assistant to stay inside one workspace.',
|
|
49
|
+
].filter(Boolean).join('\n\n');
|
|
50
|
+
// Only when tool sets are filtered, so the default answer is unchanged.
|
|
51
|
+
const toolsets = ctx.toolsets;
|
|
52
|
+
if (!toolsets || toolsets.all)
|
|
53
|
+
return text;
|
|
54
|
+
return `${text}\n\n**Tool sets:** ${toolsets.enabled.join(', ')} are listed in this connection. Every tool can still be called by name. To list every set, unset POSTERLY_TOOLSETS (or set it to all) and restart the MCP client.`;
|
|
55
|
+
}
|
|
56
|
+
async function mcpStatus(client) {
|
|
57
|
+
const apiOrigin = client.getBaseUrl();
|
|
58
|
+
const [latest, apiProbe, authProbe] = await Promise.all([
|
|
59
|
+
fetchLatestNpmVersion(),
|
|
60
|
+
probeHostedMcp(apiOrigin),
|
|
61
|
+
client.hasApiKey() ? client.probeWhoami() : Promise.resolve(null),
|
|
62
|
+
]);
|
|
63
|
+
const updateStatus = describeUpdateStatus(POSTERLY_MCP_VERSION, latest.version, latest.error);
|
|
64
|
+
const versionMatch = apiProbe.version ? yesNo(apiProbe.version === POSTERLY_MCP_VERSION) : 'unknown';
|
|
65
|
+
const rows = [
|
|
66
|
+
['Connection type', 'local app process (stdio)'],
|
|
67
|
+
['Current server version', POSTERLY_MCP_VERSION],
|
|
68
|
+
['Latest npm version', latest.version || 'unknown'],
|
|
69
|
+
['Update status', updateStatus],
|
|
70
|
+
['posterly API origin', code(apiOrigin)],
|
|
71
|
+
['MCP endpoint health', formatApiProbe(apiProbe)],
|
|
72
|
+
['Hosted MCP version', apiProbe.version || 'unknown'],
|
|
73
|
+
['Version match', versionMatch],
|
|
74
|
+
['API key configured', client.hasApiKey()],
|
|
75
|
+
['API auth check', formatAuthProbe(authProbe)],
|
|
76
|
+
];
|
|
77
|
+
if (authProbe?.ok && authProbe.whoami) {
|
|
78
|
+
rows.push(['User', authProbe.whoami.user.email || code(authProbe.whoami.user.id)], ['Default workspace', `${authProbe.whoami.default_workspace.name} (${code(authProbe.whoami.default_workspace.id)})`], ['API scopes', authProbe.whoami.api_key.scopes.join(', ') || 'none']);
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
mdTitle('posterly MCP status'),
|
|
82
|
+
mdKeyValue(rows),
|
|
83
|
+
latest.error ? `**Latest check:** ${latest.error}` : '',
|
|
84
|
+
apiProbe.error ? `**MCP endpoint check:** ${apiProbe.error}` : '',
|
|
85
|
+
authProbe && !authProbe.ok ? `**API auth check:** ${authProbe.error || 'API key validation failed'}` : '',
|
|
86
|
+
nextStep({ hasApiKey: client.hasApiKey(), updateStatus, apiProbe, authProbe }),
|
|
87
|
+
].filter(Boolean).join('\n\n');
|
|
88
|
+
}
|
|
89
|
+
async function probeHostedMcp(apiOrigin) {
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeout = setTimeout(() => controller.abort(), 2500);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${resolvePosterlyBaseUrl(apiOrigin)}/api/mcp`, {
|
|
95
|
+
headers: { Accept: 'application/json' },
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
});
|
|
98
|
+
const text = await res.text();
|
|
99
|
+
const data = parseJson(text);
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: res.status,
|
|
104
|
+
error: formatError(data, text, res.statusText),
|
|
105
|
+
durationMs: Date.now() - startedAt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const version = typeof data?.serverInfo?.version === 'string' ? data.serverInfo.version : undefined;
|
|
109
|
+
return {
|
|
110
|
+
ok: Boolean(version),
|
|
111
|
+
status: res.status,
|
|
112
|
+
version,
|
|
113
|
+
error: version ? undefined : 'Hosted MCP response did not include serverInfo.version',
|
|
114
|
+
durationMs: Date.now() - startedAt,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: err?.name === 'AbortError' ? 'timed out after 2500ms' : err?.message || String(err),
|
|
121
|
+
durationMs: Date.now() - startedAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function fetchLatestNpmVersion() {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeout = setTimeout(() => controller.abort(), 2500);
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(NPM_LATEST_URL, {
|
|
133
|
+
headers: { Accept: 'application/json' },
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
return { error: `npm registry returned HTTP ${res.status}` };
|
|
138
|
+
}
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
return typeof data.version === 'string'
|
|
141
|
+
? { version: data.version }
|
|
142
|
+
: { error: 'npm registry response did not include a version' };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const reason = err?.name === 'AbortError' ? 'timed out' : err?.message || String(err);
|
|
146
|
+
return { error: `could not check npm latest: ${reason}` };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function describeUpdateStatus(current, latest, error) {
|
|
153
|
+
if (!latest)
|
|
154
|
+
return error ? 'unknown' : 'unknown';
|
|
155
|
+
const comparison = compareSemver(current, latest);
|
|
156
|
+
if (comparison < 0) {
|
|
157
|
+
return `update available: restart the MCP client or run ${code('npx -y posterly-mcp-server@latest')}`;
|
|
158
|
+
}
|
|
159
|
+
if (comparison > 0) {
|
|
160
|
+
return 'running newer than npm latest; npm may not have been published yet';
|
|
161
|
+
}
|
|
162
|
+
return 'current';
|
|
163
|
+
}
|
|
164
|
+
function formatApiProbe(probe) {
|
|
165
|
+
if (probe.ok)
|
|
166
|
+
return `ok (HTTP ${probe.status}, ${probe.durationMs}ms)`;
|
|
167
|
+
const status = probe.status ? `HTTP ${probe.status}` : 'network';
|
|
168
|
+
return `failed (${status}, ${probe.durationMs}ms)`;
|
|
169
|
+
}
|
|
170
|
+
function formatAuthProbe(probe) {
|
|
171
|
+
if (!probe)
|
|
172
|
+
return 'skipped (POSTERLY_API_KEY not configured)';
|
|
173
|
+
if (probe.ok)
|
|
174
|
+
return `valid (HTTP ${probe.status}, ${probe.durationMs}ms)`;
|
|
175
|
+
const status = probe.status ? `HTTP ${probe.status}` : 'network';
|
|
176
|
+
return `failed (${status}, ${probe.durationMs}ms)`;
|
|
177
|
+
}
|
|
178
|
+
function nextStep(input) {
|
|
179
|
+
const { hasApiKey, updateStatus, apiProbe, authProbe } = input;
|
|
180
|
+
if (updateStatus.startsWith('update available')) {
|
|
181
|
+
return '**Next step:** Restart your MCP client so it pulls `posterly-mcp-server@latest`, then call `whoami` with `view: "status"` again.';
|
|
182
|
+
}
|
|
183
|
+
if (!apiProbe.ok) {
|
|
184
|
+
return '**Next step:** Check `POSTERLY_URL` / `POSTERLY_API_BASE_URL`, DNS, and network access to the hosted MCP endpoint, then call `whoami` with `view: "status"` again.';
|
|
185
|
+
}
|
|
186
|
+
if (!hasApiKey) {
|
|
187
|
+
return '**Next step:** Add `POSTERLY_API_KEY` after activating API and MCP access, then call `whoami`.';
|
|
188
|
+
}
|
|
189
|
+
if (authProbe && !authProbe.ok) {
|
|
190
|
+
return '**Next step:** Create or reinstall a valid posterly API key, then restart the MCP client and call `whoami` with `view: "status"` again.';
|
|
191
|
+
}
|
|
192
|
+
return '**Next step:** Call `whoami` to confirm the user, workspace, and scopes before scheduling posts.';
|
|
193
|
+
}
|
|
194
|
+
function yesNo(value) {
|
|
195
|
+
return value ? 'yes' : 'no';
|
|
196
|
+
}
|
|
197
|
+
function parseJson(text) {
|
|
198
|
+
try {
|
|
199
|
+
return text ? JSON.parse(text) : {};
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function formatError(data, fallback, statusText) {
|
|
206
|
+
if (data && typeof data === 'object') {
|
|
207
|
+
return data.error || data.message || data.code || statusText || 'API request failed';
|
|
208
|
+
}
|
|
209
|
+
return fallback || statusText || 'API request failed';
|
|
210
|
+
}
|
|
211
|
+
function compareSemver(a, b) {
|
|
212
|
+
const left = parts(a);
|
|
213
|
+
const right = parts(b);
|
|
214
|
+
for (let i = 0; i < 3; i += 1) {
|
|
215
|
+
if (left[i] !== right[i])
|
|
216
|
+
return left[i] > right[i] ? 1 : -1;
|
|
217
|
+
}
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
function parts(value) {
|
|
221
|
+
const nums = value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
222
|
+
return [
|
|
223
|
+
Number.isFinite(nums[0]) ? nums[0] : 0,
|
|
224
|
+
Number.isFinite(nums[1]) ? nums[1] : 0,
|
|
225
|
+
Number.isFinite(nums[2]) ? nums[2] : 0,
|
|
226
|
+
];
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posterly-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.1",
|
|
4
4
|
"mcpName": "io.github.awpthorp/posterly",
|
|
5
5
|
"description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
"start": "node dist/index.js",
|
|
35
35
|
"prepack": "npm run build && npm --prefix .. run --silent check:mcp-parity",
|
|
36
36
|
"test:annotations": "npm run build && node scripts/test-tool-annotations.mjs",
|
|
37
|
-
"test:payloads": "npm run build && node scripts/test-payload-builders.mjs"
|
|
37
|
+
"test:payloads": "npm run build && node scripts/test-payload-builders.mjs",
|
|
38
|
+
"test:client-header": "npm run build && node scripts/test-client-header-sanitization.mjs",
|
|
39
|
+
"test:alias-transport": "npm run build && node scripts/test-alias-transport.mjs"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@modelcontextprotocol/sdk": "
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
41
43
|
"zod": "^3.24.0"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Tests the stdio AliasTransport (src/lib/alias-transport.ts) over an
|
|
2
|
+
// in-memory transport pair with a real McpServer and Client, using a small
|
|
3
|
+
// fake registry that has aliases (the shipped registry has none yet):
|
|
4
|
+
// - aliases never appear in tools/list
|
|
5
|
+
// - an alias call is rewritten onto its target with translated args
|
|
6
|
+
// (rename + inject, inject wins) before the SDK validates it
|
|
7
|
+
// - handlers read the called name / legacyConfirm via extra.requestId
|
|
8
|
+
// - the response keeps content[0] untouched and gets exactly one appended
|
|
9
|
+
// rename note plus _meta["posterly/deprecation"]
|
|
10
|
+
// - canonical calls are untouched, including when run concurrently with an
|
|
11
|
+
// alias call (legacyConfirm never leaks across calls)
|
|
12
|
+
// - an alias call that fails per-mode validation returns a plain error
|
|
13
|
+
// (plus the note) without reaching the handler
|
|
14
|
+
// - per-request entries are cleaned up after the response, and after a
|
|
15
|
+
// notifications/cancelled (the SDK sends no response for those)
|
|
16
|
+
// - a visible set filters ONLY tools/list: hidden tools stay callable,
|
|
17
|
+
// including in the real built package with POSTERLY_TOOLSETS=core
|
|
18
|
+
// - an alias to a param-driven tool is pinned to the old tool's mode, and a
|
|
19
|
+
// legacy validator reproduces the old tool's exact schema error text
|
|
20
|
+
// Run: npm run test:alias-transport (builds first)
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
24
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
25
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
26
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
27
|
+
import { dirname, resolve } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { AliasTransport } from '../dist/lib/alias-transport.js';
|
|
30
|
+
|
|
31
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
|
|
33
|
+
const SUNSET = '2027-01-12';
|
|
34
|
+
|
|
35
|
+
const registry = {
|
|
36
|
+
version: 1,
|
|
37
|
+
designRules: 'off',
|
|
38
|
+
budgets: { maxTotal: 70, maxCore: 30 },
|
|
39
|
+
sunset: SUNSET,
|
|
40
|
+
toolsets: { core: { label: 'Core', description: 'Core tools.' } },
|
|
41
|
+
tools: {
|
|
42
|
+
manage_thing: {
|
|
43
|
+
set: 'core',
|
|
44
|
+
label: 'Manage a thing',
|
|
45
|
+
selector: 'action',
|
|
46
|
+
actions: {
|
|
47
|
+
rename: {
|
|
48
|
+
label: 'Rename a thing',
|
|
49
|
+
destructive: false,
|
|
50
|
+
idempotent: true,
|
|
51
|
+
openWorld: false,
|
|
52
|
+
confirm: 'confirm',
|
|
53
|
+
params: {
|
|
54
|
+
thing_id: { type: 'string', required: true },
|
|
55
|
+
title: { type: 'string', required: true },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
delete: { label: 'Delete a thing', destructive: true, idempotent: true, openWorld: false, confirm: 'preview' },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
slow_thing: { set: 'core', label: 'Slow thing', readOnly: true, destructive: false, idempotent: true, openWorld: false, confirm: 'none' },
|
|
62
|
+
list_things: {
|
|
63
|
+
set: 'core',
|
|
64
|
+
label: 'Things',
|
|
65
|
+
selector: 'view',
|
|
66
|
+
defaultMode: 'all',
|
|
67
|
+
views: {
|
|
68
|
+
all: { label: 'All things' },
|
|
69
|
+
one: { label: 'One thing', params: { thing_id: { type: 'string', required: true }, limit: { type: 'integer', min: 1, max: 50, default: 10 } } },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
aliases: {
|
|
74
|
+
old_slow: {
|
|
75
|
+
target: 'slow_thing',
|
|
76
|
+
added: '2026-10-01',
|
|
77
|
+
sunset: SUNSET,
|
|
78
|
+
},
|
|
79
|
+
delete_thing: {
|
|
80
|
+
target: 'manage_thing',
|
|
81
|
+
inject: { action: 'delete' },
|
|
82
|
+
rename: { id: 'thing_id' },
|
|
83
|
+
legacyConfirm: true,
|
|
84
|
+
added: '2026-10-01',
|
|
85
|
+
sunset: SUNSET,
|
|
86
|
+
},
|
|
87
|
+
rename_thing: {
|
|
88
|
+
target: 'manage_thing',
|
|
89
|
+
inject: { action: 'rename' },
|
|
90
|
+
added: '2026-10-01',
|
|
91
|
+
sunset: SUNSET,
|
|
92
|
+
},
|
|
93
|
+
get_thing: {
|
|
94
|
+
target: 'list_things',
|
|
95
|
+
inject: { view: 'one' },
|
|
96
|
+
added: '2026-10-01',
|
|
97
|
+
sunset: SUNSET,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const server = new McpServer({ name: 'alias-transport-test', version: '0.0.0' });
|
|
103
|
+
const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
|
|
104
|
+
const transport = new AliasTransport(serverSide, registry);
|
|
105
|
+
|
|
106
|
+
const seen = [];
|
|
107
|
+
server.tool(
|
|
108
|
+
'manage_thing',
|
|
109
|
+
'Rename or delete a thing.',
|
|
110
|
+
{
|
|
111
|
+
action: z.enum(['rename', 'delete']),
|
|
112
|
+
thing_id: z.string(),
|
|
113
|
+
title: z.string().optional(),
|
|
114
|
+
confirm: z.boolean().optional(),
|
|
115
|
+
},
|
|
116
|
+
async (input, extra) => {
|
|
117
|
+
const info = transport.getCallInfo(extra.requestId);
|
|
118
|
+
seen.push({ tool: 'manage_thing', input, calledName: info?.calledName ?? null, legacyConfirm: info?.legacyConfirm ?? false });
|
|
119
|
+
// Give a concurrent call the chance to interleave.
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
121
|
+
return { content: [{ type: 'text', text: `did ${input.action} on ${input.thing_id}` }] };
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
server.tool(
|
|
125
|
+
'list_things',
|
|
126
|
+
'List things.',
|
|
127
|
+
{ view: z.enum(['all', 'one']).optional(), thing_id: z.string().optional(), limit: z.number().optional() },
|
|
128
|
+
async (input, extra) => {
|
|
129
|
+
const info = transport.getCallInfo(extra.requestId);
|
|
130
|
+
seen.push({ tool: 'list_things', input, calledName: info?.calledName ?? null, legacyConfirm: info?.legacyConfirm ?? false });
|
|
131
|
+
return { content: [{ type: 'text', text: `view ${input.view} ${input.thing_id ?? ''} ${input.limit ?? ''}`.trim() }] };
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
let slowRequestId;
|
|
136
|
+
server.tool('slow_thing', 'Takes a while.', {}, async (_input, extra) => {
|
|
137
|
+
slowRequestId = extra.requestId;
|
|
138
|
+
await new Promise((resolve) => {
|
|
139
|
+
const timer = setTimeout(resolve, 500);
|
|
140
|
+
extra.signal.addEventListener('abort', () => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
return { content: [{ type: 'text', text: 'slow done' }] };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const client = new Client({ name: 'alias-transport-test-client', version: '0.0.0' });
|
|
149
|
+
await server.connect(transport);
|
|
150
|
+
await client.connect(clientSide);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// 1. Aliases never appear in tools/list.
|
|
154
|
+
const { tools } = await client.listTools();
|
|
155
|
+
assert.deepEqual(tools.map((tool) => tool.name).sort(), ['list_things', 'manage_thing', 'slow_thing']);
|
|
156
|
+
|
|
157
|
+
// 2. Alias call: rename + inject (inject wins over a caller-supplied action).
|
|
158
|
+
seen.length = 0;
|
|
159
|
+
const aliased = await client.callTool({ name: 'delete_thing', arguments: { id: 't_1', action: 'rename', confirm: true } });
|
|
160
|
+
assert.equal(seen.length, 1);
|
|
161
|
+
assert.deepEqual(seen[0].input, { action: 'delete', thing_id: 't_1', confirm: true });
|
|
162
|
+
assert.equal(seen[0].calledName, 'delete_thing', 'handler reads the called (alias) name via extra.requestId');
|
|
163
|
+
assert.equal(seen[0].legacyConfirm, true);
|
|
164
|
+
assert.equal(aliased.content.length, 2, 'exactly one appended deprecation item');
|
|
165
|
+
assert.deepEqual(aliased.content[0], { type: 'text', text: 'did delete on t_1' }, 'content[0] is untouched');
|
|
166
|
+
assert.equal(
|
|
167
|
+
aliased.content[1].text,
|
|
168
|
+
`Renamed: delete_thing is now manage_thing with action=delete. The old name may stop working after ${SUNSET}.`,
|
|
169
|
+
);
|
|
170
|
+
assert.deepEqual(aliased._meta?.['posterly/deprecation'], {
|
|
171
|
+
old: 'delete_thing',
|
|
172
|
+
new: 'manage_thing',
|
|
173
|
+
action: 'delete',
|
|
174
|
+
sunset: SUNSET,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 3. Canonical call: untouched, no note, no _meta, no alias info.
|
|
178
|
+
seen.length = 0;
|
|
179
|
+
const canonical = await client.callTool({ name: 'manage_thing', arguments: { action: 'rename', thing_id: 't_2', title: 'New' } });
|
|
180
|
+
assert.equal(canonical.content.length, 1);
|
|
181
|
+
assert.equal(canonical._meta, undefined);
|
|
182
|
+
assert.equal(seen[0].calledName, null);
|
|
183
|
+
assert.equal(seen[0].legacyConfirm, false);
|
|
184
|
+
|
|
185
|
+
// 4. Concurrent alias + canonical calls: legacyConfirm never leaks.
|
|
186
|
+
seen.length = 0;
|
|
187
|
+
const [a, b] = await Promise.all([
|
|
188
|
+
client.callTool({ name: 'delete_thing', arguments: { id: 't_3', confirm: true } }),
|
|
189
|
+
client.callTool({ name: 'manage_thing', arguments: { action: 'delete', thing_id: 't_4' } }),
|
|
190
|
+
]);
|
|
191
|
+
const byThing = Object.fromEntries(seen.map((entry) => [entry.input.thing_id, entry]));
|
|
192
|
+
assert.equal(byThing.t_3.legacyConfirm, true);
|
|
193
|
+
assert.equal(byThing.t_3.calledName, 'delete_thing');
|
|
194
|
+
assert.equal(byThing.t_4.legacyConfirm, false);
|
|
195
|
+
assert.equal(byThing.t_4.calledName, null);
|
|
196
|
+
assert.equal(a.content.length, 2);
|
|
197
|
+
assert.equal(b.content.length, 1);
|
|
198
|
+
|
|
199
|
+
// 5. Per-mode validation (registry params) normalises before the SDK sees it:
|
|
200
|
+
// the default limit is applied.
|
|
201
|
+
seen.length = 0;
|
|
202
|
+
const viewed = await client.callTool({ name: 'get_thing', arguments: { thing_id: 't_5' } });
|
|
203
|
+
assert.deepEqual(seen[0].input, { view: 'one', thing_id: 't_5', limit: 10 });
|
|
204
|
+
assert.equal(viewed.content[0].text, 'view one t_5 10');
|
|
205
|
+
assert.equal(viewed._meta?.['posterly/deprecation']?.action, 'one');
|
|
206
|
+
|
|
207
|
+
// 6. A validation failure is a plain error plus the note; the handler never runs.
|
|
208
|
+
seen.length = 0;
|
|
209
|
+
const invalid = await client.callTool({ name: 'rename_thing', arguments: { thing_id: 't_6' } });
|
|
210
|
+
assert.equal(seen.length, 0, 'handler must not run when validation fails');
|
|
211
|
+
assert.equal(invalid.isError, true);
|
|
212
|
+
assert.equal(invalid.content[0].text, 'title is required with action=rename.');
|
|
213
|
+
assert.equal(invalid.content.length, 2);
|
|
214
|
+
const extra = await client.callTool({ name: 'rename_thing', arguments: { thing_id: 't_6', title: 'x', bogus: 1 } });
|
|
215
|
+
assert.equal(extra.isError, true);
|
|
216
|
+
assert.equal(extra.content[0].text, 'bogus is not allowed with action=rename.');
|
|
217
|
+
|
|
218
|
+
// 7. Entries are cleaned up after each response.
|
|
219
|
+
for (let id = 0; id < 50; id += 1) {
|
|
220
|
+
assert.equal(transport.getCallInfo(id), undefined, `request ${id} still tracked after its response`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 8. A cancelled alias call (no response is ever sent) is still cleaned up.
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const pendingSlow = client.callTool({ name: 'old_slow', arguments: {} }, undefined, { signal: controller.signal });
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
227
|
+
assert.notEqual(slowRequestId, undefined, 'slow handler started');
|
|
228
|
+
assert.equal(transport.getCallInfo(slowRequestId)?.calledName, 'old_slow', 'tracked while in flight');
|
|
229
|
+
controller.abort();
|
|
230
|
+
await assert.rejects(pendingSlow);
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
232
|
+
assert.equal(transport.getCallInfo(slowRequestId), undefined, 'notifications/cancelled clears the entry');
|
|
233
|
+
} finally {
|
|
234
|
+
await client.close();
|
|
235
|
+
await server.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 9. A visible set filters tools/list only; hidden tools stay callable.
|
|
239
|
+
{
|
|
240
|
+
const filteredServer = new McpServer({ name: 'alias-transport-filter-test', version: '0.0.0' });
|
|
241
|
+
const [filteredClientSide, filteredServerSide] = InMemoryTransport.createLinkedPair();
|
|
242
|
+
const filteredTransport = new AliasTransport(filteredServerSide, registry, new Set(['list_things']));
|
|
243
|
+
filteredServer.tool('list_things', 'List things.', {}, async () => ({ content: [{ type: 'text', text: 'listed' }] }));
|
|
244
|
+
filteredServer.tool('manage_thing', 'Manage a thing.', { action: z.string(), thing_id: z.string() }, async (input) => ({
|
|
245
|
+
content: [{ type: 'text', text: `managed ${input.thing_id}` }],
|
|
246
|
+
}));
|
|
247
|
+
const filteredClient = new Client({ name: 'alias-transport-filter-client', version: '0.0.0' });
|
|
248
|
+
await filteredServer.connect(filteredTransport);
|
|
249
|
+
await filteredClient.connect(filteredClientSide);
|
|
250
|
+
try {
|
|
251
|
+
const { tools: listed } = await filteredClient.listTools();
|
|
252
|
+
assert.deepEqual(listed.map((tool) => tool.name), ['list_things'], 'hidden tool is not listed');
|
|
253
|
+
const hiddenCall = await filteredClient.callTool({ name: 'manage_thing', arguments: { action: 'delete', thing_id: 't_9' } });
|
|
254
|
+
assert.equal(hiddenCall.isError, undefined);
|
|
255
|
+
assert.equal(hiddenCall.content[0].text, 'managed t_9', 'hidden tool is still callable');
|
|
256
|
+
const aliasToHidden = await filteredClient.callTool({ name: 'delete_thing', arguments: { id: 't_10' } });
|
|
257
|
+
assert.equal(aliasToHidden.content[0].text, 'managed t_10', 'an alias to a hidden tool still works');
|
|
258
|
+
} finally {
|
|
259
|
+
await filteredClient.close();
|
|
260
|
+
await filteredServer.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 10. The real built package: POSTERLY_TOOLSETS=core hides get_subscription
|
|
265
|
+
// (billing) from tools/list, but calling it still reaches the tool. With
|
|
266
|
+
// no API key the tool answers its own "not installed" error before any
|
|
267
|
+
// network request, which proves it ran (an unregistered tool would give
|
|
268
|
+
// the SDK's "Tool get_subscription not found").
|
|
269
|
+
{
|
|
270
|
+
const stdioClient = new Client({ name: 'alias-transport-toolsets-client', version: '0.0.0' });
|
|
271
|
+
const stdioTransport = new StdioClientTransport({
|
|
272
|
+
command: process.execPath,
|
|
273
|
+
args: [resolve(packageRoot, 'dist/index.js')],
|
|
274
|
+
env: { POSTERLY_TOOLSETS: 'core' },
|
|
275
|
+
stderr: 'ignore',
|
|
276
|
+
});
|
|
277
|
+
await stdioClient.connect(stdioTransport);
|
|
278
|
+
try {
|
|
279
|
+
const { tools: coreTools } = await stdioClient.listTools();
|
|
280
|
+
const names = coreTools.map((tool) => tool.name);
|
|
281
|
+
assert(!names.includes('get_subscription'), 'billing tool hidden from tools/list under core');
|
|
282
|
+
assert(names.includes('create_post'), 'core tool listed');
|
|
283
|
+
assert(names.includes('start_signup'), 'signup tools are always listed on stdio');
|
|
284
|
+
const hidden = await stdioClient.callTool({ name: 'get_subscription', arguments: {} });
|
|
285
|
+
assert.equal(hidden.isError, true);
|
|
286
|
+
assert.match(hidden.content[0].text, /posterly access is not installed yet/, 'the hidden tool itself answered');
|
|
287
|
+
} finally {
|
|
288
|
+
await stdioClient.close();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 11. Param-driven targets and legacy schemas (mcp 0.45.0): an alias with
|
|
293
|
+
// `mode` is validated in the old tool's mode (a missing id is rejected,
|
|
294
|
+
// never silently widened into a list), and a legacy validator reproduces
|
|
295
|
+
// the old tool's exact error text before any translation.
|
|
296
|
+
{
|
|
297
|
+
const modeRegistry = {
|
|
298
|
+
...registry,
|
|
299
|
+
tools: {
|
|
300
|
+
list_notes: {
|
|
301
|
+
set: 'core',
|
|
302
|
+
label: 'Notes',
|
|
303
|
+
selector: 'params',
|
|
304
|
+
defaultMode: 'list',
|
|
305
|
+
views: {
|
|
306
|
+
list: { label: 'List notes', params: { limit: { type: 'integer', min: 1, max: 50 } } },
|
|
307
|
+
note: { label: 'One note', when: ['note_id'], params: { note_id: { type: 'string', required: true } } },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
aliases: {
|
|
312
|
+
get_note: { target: 'list_notes', mode: 'note', added: '2026-10-01', sunset: SUNSET },
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
const legacyValidator = (aliasName, args) => {
|
|
316
|
+
if (aliasName !== 'get_note') return null;
|
|
317
|
+
if (!args || typeof args.note_id !== 'string') return { ok: false, message: 'MCP error -32602: old get_note schema error' };
|
|
318
|
+
return { ok: true, args: { note_id: args.note_id } };
|
|
319
|
+
};
|
|
320
|
+
const modeServer = new McpServer({ name: 'alias-transport-mode-test', version: '0.0.0' });
|
|
321
|
+
const [modeClientSide, modeServerSide] = InMemoryTransport.createLinkedPair();
|
|
322
|
+
const modeTransport = new AliasTransport(modeServerSide, modeRegistry, null, legacyValidator);
|
|
323
|
+
const noteCalls = [];
|
|
324
|
+
modeServer.tool('list_notes', 'Notes.', { note_id: z.string().optional(), limit: z.number().optional() }, async (input) => {
|
|
325
|
+
noteCalls.push(input);
|
|
326
|
+
return { content: [{ type: 'text', text: input.note_id ? `note ${input.note_id}` : 'all notes' }] };
|
|
327
|
+
});
|
|
328
|
+
const modeClient = new Client({ name: 'alias-transport-mode-client', version: '0.0.0' });
|
|
329
|
+
await modeServer.connect(modeTransport);
|
|
330
|
+
await modeClient.connect(modeClientSide);
|
|
331
|
+
try {
|
|
332
|
+
const legacyError = await modeClient.callTool({ name: 'get_note', arguments: {} });
|
|
333
|
+
assert.equal(legacyError.isError, true);
|
|
334
|
+
assert.equal(legacyError.content[0].text, 'MCP error -32602: old get_note schema error', 'the old schema error text is kept');
|
|
335
|
+
assert.equal(legacyError.content.length, 2, 'the rename note is still appended');
|
|
336
|
+
assert.equal(noteCalls.length, 0, 'the handler never ran');
|
|
337
|
+
const one = await modeClient.callTool({ name: 'get_note', arguments: { note_id: 'n_1', extra: 'dropped' } });
|
|
338
|
+
assert.equal(one.content[0].text, 'note n_1');
|
|
339
|
+
assert.deepEqual(noteCalls, [{ note_id: 'n_1' }], 'the legacy-parsed args (unknown keys dropped) reach the handler');
|
|
340
|
+
} finally {
|
|
341
|
+
await modeClient.close();
|
|
342
|
+
await modeServer.close();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Without a legacy validator, the pinned mode still rejects a missing id.
|
|
346
|
+
const pinServer = new McpServer({ name: 'alias-transport-pin-test', version: '0.0.0' });
|
|
347
|
+
const [pinClientSide, pinServerSide] = InMemoryTransport.createLinkedPair();
|
|
348
|
+
const pinTransport = new AliasTransport(pinServerSide, modeRegistry);
|
|
349
|
+
let pinHandlerRan = false;
|
|
350
|
+
pinServer.tool('list_notes', 'Notes.', { note_id: z.string().optional() }, async () => {
|
|
351
|
+
pinHandlerRan = true;
|
|
352
|
+
return { content: [{ type: 'text', text: 'all notes' }] };
|
|
353
|
+
});
|
|
354
|
+
const pinClient = new Client({ name: 'alias-transport-pin-client', version: '0.0.0' });
|
|
355
|
+
await pinServer.connect(pinTransport);
|
|
356
|
+
await pinClient.connect(pinClientSide);
|
|
357
|
+
try {
|
|
358
|
+
const pinned = await pinClient.callTool({ name: 'get_note', arguments: {} });
|
|
359
|
+
assert.equal(pinned.isError, true);
|
|
360
|
+
assert.equal(pinned.content[0].text, 'note_id is required.');
|
|
361
|
+
assert.equal(pinHandlerRan, false, 'get_note without note_id never becomes a list call');
|
|
362
|
+
} finally {
|
|
363
|
+
await pinClient.close();
|
|
364
|
+
await pinServer.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log('OK alias transport: hidden aliases, translated args, appended note, no leaks, tool sets filter tools/list only, pinned modes, legacy schemas.');
|