posterly-mcp-server 0.44.0 → 0.45.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.
- 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 +6 -6
- package/dist/tools/create-post.js +3 -3
- package/dist/tools/create-posts-batch.d.ts +4 -4
- 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/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-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 +3 -3
- 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/whoami.ts +253 -30
- package/tool-annotations.json +76 -149
- package/tool-registry.json +2085 -0
- 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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport wrapper that keeps renamed MCP tools working on the stdio package
|
|
3
|
+
* without registering the old names with McpServer (so they never appear in
|
|
4
|
+
* tools/list).
|
|
5
|
+
*
|
|
6
|
+
* Incoming: a tools/call whose name is an alias is rewritten BEFORE McpServer
|
|
7
|
+
* sees it: params.name becomes the target tool and params.arguments become
|
|
8
|
+
* the translated arguments, so the SDK validates them against the canonical
|
|
9
|
+
* schema. The request id is remembered with the alias details.
|
|
10
|
+
*
|
|
11
|
+
* Handlers read that per-request info through getCallInfo(extra.requestId):
|
|
12
|
+
* the called name (for the X-Posterly-Tool telemetry header) and whether the
|
|
13
|
+
* alias may still confirm in one step. Nothing is stored in the arguments.
|
|
14
|
+
*
|
|
15
|
+
* Outgoing: the response for that id gets the rename note APPENDED as a final
|
|
16
|
+
* text item (content[0] is never touched) plus _meta["posterly/deprecation"],
|
|
17
|
+
* and the remembered entry is deleted. A notifications/cancelled for that id
|
|
18
|
+
* also deletes it (the SDK sends no response for a cancelled request).
|
|
19
|
+
*
|
|
20
|
+
* Legacy input schemas: when a legacyValidator is given, an alias call's
|
|
21
|
+
* arguments are first checked against the OLD tool's exact input schema, so a
|
|
22
|
+
* call the old tool rejected gets the same SDK validation error text (and a
|
|
23
|
+
* call it accepted gets the same parsed arguments, defaults included) before
|
|
24
|
+
* translation. The npm package passes the old zod schemas here.
|
|
25
|
+
*
|
|
26
|
+
* Tool sets: every tool stays registered (so a call to any tool always
|
|
27
|
+
* works); when a visible set is given, only the outgoing tools/list result is
|
|
28
|
+
* filtered to it.
|
|
29
|
+
*
|
|
30
|
+
* Only the public Transport interface is used; no SDK private fields.
|
|
31
|
+
*/
|
|
32
|
+
import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
33
|
+
import type { JSONRPCMessage, MessageExtraInfo, RequestId } from '@modelcontextprotocol/sdk/types.js';
|
|
34
|
+
import { type ResolvedAlias, type ToolRegistry } from './tool-registry.js';
|
|
35
|
+
export interface AliasCallInfo {
|
|
36
|
+
/** The name the client called (the alias). */
|
|
37
|
+
calledName: string;
|
|
38
|
+
/** The canonical tool the call was routed to. */
|
|
39
|
+
target: string;
|
|
40
|
+
/** The alias may confirm with confirm:true alone until its sunset date. */
|
|
41
|
+
legacyConfirm: boolean;
|
|
42
|
+
alias: ResolvedAlias;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Checks an alias call's raw arguments against the old tool's input schema.
|
|
46
|
+
* Return null when the alias has no legacy schema (no check), the parsed
|
|
47
|
+
* arguments when they are valid, or the exact error text the old tool gave.
|
|
48
|
+
*/
|
|
49
|
+
export type LegacyAliasValidator = (aliasName: string, args: unknown) => {
|
|
50
|
+
ok: true;
|
|
51
|
+
args: Record<string, unknown>;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
message: string;
|
|
55
|
+
} | null;
|
|
56
|
+
export declare class AliasTransport implements Transport {
|
|
57
|
+
private readonly inner;
|
|
58
|
+
private readonly registry;
|
|
59
|
+
private readonly visibleTools;
|
|
60
|
+
private readonly legacyValidator;
|
|
61
|
+
onclose?: () => void;
|
|
62
|
+
onerror?: (error: Error) => void;
|
|
63
|
+
onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
|
|
64
|
+
private readonly calls;
|
|
65
|
+
/** In-flight tools/list request ids, so only their responses are filtered. */
|
|
66
|
+
private readonly listRequests;
|
|
67
|
+
/**
|
|
68
|
+
* @param visibleTools names to list in tools/list, or null to list every
|
|
69
|
+
* registered tool unchanged (the default: every set).
|
|
70
|
+
*/
|
|
71
|
+
constructor(inner: Transport, registry: ToolRegistry, visibleTools?: Set<string> | null, legacyValidator?: LegacyAliasValidator | null);
|
|
72
|
+
get sessionId(): string | undefined;
|
|
73
|
+
setProtocolVersion(version: string): void;
|
|
74
|
+
start(): Promise<void>;
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
/** Alias details for an in-flight tools/call, or undefined for a canonical call. */
|
|
77
|
+
getCallInfo(requestId: RequestId | undefined): AliasCallInfo | undefined;
|
|
78
|
+
send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void>;
|
|
79
|
+
private handleIncoming;
|
|
80
|
+
/** Answers an alias call directly with a plain isError result; send() appends the rename note. */
|
|
81
|
+
private respondWithError;
|
|
82
|
+
private filterToolsList;
|
|
83
|
+
private decorateResponse;
|
|
84
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport wrapper that keeps renamed MCP tools working on the stdio package
|
|
3
|
+
* without registering the old names with McpServer (so they never appear in
|
|
4
|
+
* tools/list).
|
|
5
|
+
*
|
|
6
|
+
* Incoming: a tools/call whose name is an alias is rewritten BEFORE McpServer
|
|
7
|
+
* sees it: params.name becomes the target tool and params.arguments become
|
|
8
|
+
* the translated arguments, so the SDK validates them against the canonical
|
|
9
|
+
* schema. The request id is remembered with the alias details.
|
|
10
|
+
*
|
|
11
|
+
* Handlers read that per-request info through getCallInfo(extra.requestId):
|
|
12
|
+
* the called name (for the X-Posterly-Tool telemetry header) and whether the
|
|
13
|
+
* alias may still confirm in one step. Nothing is stored in the arguments.
|
|
14
|
+
*
|
|
15
|
+
* Outgoing: the response for that id gets the rename note APPENDED as a final
|
|
16
|
+
* text item (content[0] is never touched) plus _meta["posterly/deprecation"],
|
|
17
|
+
* and the remembered entry is deleted. A notifications/cancelled for that id
|
|
18
|
+
* also deletes it (the SDK sends no response for a cancelled request).
|
|
19
|
+
*
|
|
20
|
+
* Legacy input schemas: when a legacyValidator is given, an alias call's
|
|
21
|
+
* arguments are first checked against the OLD tool's exact input schema, so a
|
|
22
|
+
* call the old tool rejected gets the same SDK validation error text (and a
|
|
23
|
+
* call it accepted gets the same parsed arguments, defaults included) before
|
|
24
|
+
* translation. The npm package passes the old zod schemas here.
|
|
25
|
+
*
|
|
26
|
+
* Tool sets: every tool stays registered (so a call to any tool always
|
|
27
|
+
* works); when a visible set is given, only the outgoing tools/list result is
|
|
28
|
+
* filtered to it.
|
|
29
|
+
*
|
|
30
|
+
* Only the public Transport interface is used; no SDK private fields.
|
|
31
|
+
*/
|
|
32
|
+
import { DEPRECATION_META_KEY, deprecationNote, resolveAlias, translateArgs, validateMode, } from './tool-registry.js';
|
|
33
|
+
function requestKey(id) {
|
|
34
|
+
return `${typeof id}:${String(id)}`;
|
|
35
|
+
}
|
|
36
|
+
function isObject(value) {
|
|
37
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
export class AliasTransport {
|
|
40
|
+
inner;
|
|
41
|
+
registry;
|
|
42
|
+
visibleTools;
|
|
43
|
+
legacyValidator;
|
|
44
|
+
onclose;
|
|
45
|
+
onerror;
|
|
46
|
+
onmessage;
|
|
47
|
+
calls = new Map();
|
|
48
|
+
/** In-flight tools/list request ids, so only their responses are filtered. */
|
|
49
|
+
listRequests = new Set();
|
|
50
|
+
/**
|
|
51
|
+
* @param visibleTools names to list in tools/list, or null to list every
|
|
52
|
+
* registered tool unchanged (the default: every set).
|
|
53
|
+
*/
|
|
54
|
+
constructor(inner, registry, visibleTools = null, legacyValidator = null) {
|
|
55
|
+
this.inner = inner;
|
|
56
|
+
this.registry = registry;
|
|
57
|
+
this.visibleTools = visibleTools;
|
|
58
|
+
this.legacyValidator = legacyValidator;
|
|
59
|
+
inner.onmessage = (message, extra) => this.handleIncoming(message, extra);
|
|
60
|
+
inner.onclose = () => this.onclose?.();
|
|
61
|
+
inner.onerror = (error) => this.onerror?.(error);
|
|
62
|
+
}
|
|
63
|
+
get sessionId() {
|
|
64
|
+
return this.inner.sessionId;
|
|
65
|
+
}
|
|
66
|
+
setProtocolVersion(version) {
|
|
67
|
+
this.inner.setProtocolVersion?.(version);
|
|
68
|
+
}
|
|
69
|
+
start() {
|
|
70
|
+
return this.inner.start();
|
|
71
|
+
}
|
|
72
|
+
close() {
|
|
73
|
+
this.calls.clear();
|
|
74
|
+
this.listRequests.clear();
|
|
75
|
+
return this.inner.close();
|
|
76
|
+
}
|
|
77
|
+
/** Alias details for an in-flight tools/call, or undefined for a canonical call. */
|
|
78
|
+
getCallInfo(requestId) {
|
|
79
|
+
if (requestId === undefined || requestId === null)
|
|
80
|
+
return undefined;
|
|
81
|
+
return this.calls.get(requestKey(requestId));
|
|
82
|
+
}
|
|
83
|
+
async send(message, options) {
|
|
84
|
+
const outgoing = this.filterToolsList(this.decorateResponse(message));
|
|
85
|
+
return this.inner.send(outgoing, options);
|
|
86
|
+
}
|
|
87
|
+
handleIncoming(message, extra) {
|
|
88
|
+
const request = message;
|
|
89
|
+
if (request.method === 'notifications/cancelled') {
|
|
90
|
+
const cancelled = isObject(request.params) ? request.params.requestId : undefined;
|
|
91
|
+
if (typeof cancelled === 'string' || typeof cancelled === 'number') {
|
|
92
|
+
this.calls.delete(requestKey(cancelled));
|
|
93
|
+
this.listRequests.delete(requestKey(cancelled));
|
|
94
|
+
}
|
|
95
|
+
this.onmessage?.(message, extra);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (request.method === 'tools/list' && this.visibleTools && request.id !== undefined && request.id !== null) {
|
|
99
|
+
this.listRequests.add(requestKey(request.id));
|
|
100
|
+
this.onmessage?.(message, extra);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (request.method !== 'tools/call' || request.id === undefined || request.id === null || !isObject(request.params)) {
|
|
104
|
+
this.onmessage?.(message, extra);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const name = request.params.name;
|
|
108
|
+
const alias = typeof name === 'string' ? resolveAlias(this.registry, name) : null;
|
|
109
|
+
if (!alias) {
|
|
110
|
+
this.onmessage?.(message, extra);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const info = {
|
|
114
|
+
calledName: alias.name,
|
|
115
|
+
target: alias.target,
|
|
116
|
+
legacyConfirm: alias.legacyConfirm === true,
|
|
117
|
+
alias,
|
|
118
|
+
};
|
|
119
|
+
this.calls.set(requestKey(request.id), info);
|
|
120
|
+
let callerArgs = request.params.arguments;
|
|
121
|
+
const legacy = this.legacyValidator ? this.legacyValidator(alias.name, callerArgs) : null;
|
|
122
|
+
if (legacy && !legacy.ok) {
|
|
123
|
+
this.respondWithError(request.id, legacy.message);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (legacy?.ok)
|
|
127
|
+
callerArgs = legacy.args;
|
|
128
|
+
// alias.mode pins a param-driven target to the old tool's mode.
|
|
129
|
+
const validated = validateMode(this.registry, alias.target, translateArgs(alias, callerArgs), alias.mode);
|
|
130
|
+
if (!validated.ok) {
|
|
131
|
+
// A plain error before any request, same as the hosted endpoint; the
|
|
132
|
+
// rename note is still appended by send() so the caller learns the new
|
|
133
|
+
// name either way.
|
|
134
|
+
this.respondWithError(request.id, validated.error);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const rewritten = {
|
|
138
|
+
...message,
|
|
139
|
+
params: { ...request.params, name: alias.target, arguments: validated.args },
|
|
140
|
+
};
|
|
141
|
+
this.onmessage?.(rewritten, extra);
|
|
142
|
+
}
|
|
143
|
+
/** Answers an alias call directly with a plain isError result; send() appends the rename note. */
|
|
144
|
+
respondWithError(id, text) {
|
|
145
|
+
void this.send({
|
|
146
|
+
jsonrpc: '2.0',
|
|
147
|
+
id,
|
|
148
|
+
result: { content: [{ type: 'text', text }], isError: true },
|
|
149
|
+
}).catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));
|
|
150
|
+
}
|
|
151
|
+
filterToolsList(message) {
|
|
152
|
+
const response = message;
|
|
153
|
+
if (!this.visibleTools || response.method !== undefined || response.id === undefined || response.id === null)
|
|
154
|
+
return message;
|
|
155
|
+
const key = requestKey(response.id);
|
|
156
|
+
if (!this.listRequests.has(key))
|
|
157
|
+
return message;
|
|
158
|
+
this.listRequests.delete(key);
|
|
159
|
+
if (!isObject(response.result) || !Array.isArray(response.result.tools))
|
|
160
|
+
return message;
|
|
161
|
+
const visible = this.visibleTools;
|
|
162
|
+
const filtered = {
|
|
163
|
+
...message,
|
|
164
|
+
result: {
|
|
165
|
+
...response.result,
|
|
166
|
+
tools: response.result.tools.filter((tool) => typeof tool?.name === 'string' && visible.has(tool.name)),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
return filtered;
|
|
170
|
+
}
|
|
171
|
+
decorateResponse(message) {
|
|
172
|
+
const response = message;
|
|
173
|
+
if (response.method !== undefined || response.id === undefined || response.id === null)
|
|
174
|
+
return message;
|
|
175
|
+
const key = requestKey(response.id);
|
|
176
|
+
const info = this.calls.get(key);
|
|
177
|
+
if (!info)
|
|
178
|
+
return message;
|
|
179
|
+
this.calls.delete(key);
|
|
180
|
+
if (!isObject(response.result) || !Array.isArray(response.result.content))
|
|
181
|
+
return message;
|
|
182
|
+
const note = deprecationNote(info.alias);
|
|
183
|
+
const decorated = {
|
|
184
|
+
...message,
|
|
185
|
+
result: {
|
|
186
|
+
...response.result,
|
|
187
|
+
content: [...response.result.content, { type: 'text', text: note.text }],
|
|
188
|
+
_meta: { ...(isObject(response.result._meta) ? response.result._meta : {}), [DEPRECATION_META_KEY]: note.meta },
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
return decorated;
|
|
192
|
+
}
|
|
193
|
+
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export declare function setMcpClientNameResolver(resolver: () => string | undefined): void;
|
|
2
|
+
/**
|
|
3
|
+
* HTTP header values must be printable Latin-1 with no control characters;
|
|
4
|
+
* Node's fetch throws (not returns an error, THROWS) on a header value
|
|
5
|
+
* containing anything else, so an MCP client reporting a non-ASCII name
|
|
6
|
+
* (e.g. "通义灵码") or one with an en/em dash would otherwise break every
|
|
7
|
+
* single tool call. Strips anything outside printable ASCII rather than
|
|
8
|
+
* encoding it, since this is just a display label, not data that needs to
|
|
9
|
+
* round-trip losslessly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeClientHeaderValue(value: string): string;
|
|
1
12
|
export interface Account {
|
|
2
13
|
id: string;
|
|
3
14
|
platform: string;
|
|
@@ -1049,6 +1060,13 @@ export declare class PosterlyClient {
|
|
|
1049
1060
|
constructor(apiKey?: string, baseUrl?: string);
|
|
1050
1061
|
hasApiKey(): boolean;
|
|
1051
1062
|
getBaseUrl(): string;
|
|
1063
|
+
/**
|
|
1064
|
+
* Runs `fn` with a fresh per-call tool name/id scoped via AsyncLocalStorage,
|
|
1065
|
+
* so any request() calls made inside it (directly or nested) are tagged
|
|
1066
|
+
* with X-Posterly-Tool / X-Posterly-Tool-Call. Called once per tool
|
|
1067
|
+
* invocation from index.ts's server.tool() handlers.
|
|
1068
|
+
*/
|
|
1069
|
+
runToolCall<T>(toolName: string, fn: () => Promise<T>): Promise<T>;
|
|
1052
1070
|
probeWhoami(timeoutMs?: number): Promise<{
|
|
1053
1071
|
ok: boolean;
|
|
1054
1072
|
status?: number;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Scopes the current tool name/call id to the async execution of a single
|
|
7
|
+
* tool call, so request() can tag its headers without a mutable "current
|
|
8
|
+
* tool" field on the shared PosterlyClient singleton (index.ts registers one
|
|
9
|
+
* client instance for every tool). AsyncLocalStorage keeps overlapping calls
|
|
10
|
+
* from clobbering each other's context if the stdio transport ever
|
|
11
|
+
* dispatches tool calls concurrently.
|
|
12
|
+
*/
|
|
13
|
+
const toolCallStorage = new AsyncLocalStorage();
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the connected MCP client's name (e.g. "claude-ai", "chatgpt"), set
|
|
16
|
+
* once by index.ts after the server is constructed. A resolver function
|
|
17
|
+
* rather than a static value because the SDK only learns the client's
|
|
18
|
+
* identity once the initialize handshake completes, which happens after this
|
|
19
|
+
* module is first loaded; reading through the resolver on every request
|
|
20
|
+
* always gets the current value instead of whatever was available at import
|
|
21
|
+
* time (undefined).
|
|
22
|
+
*/
|
|
23
|
+
let clientNameResolver = null;
|
|
24
|
+
export function setMcpClientNameResolver(resolver) {
|
|
25
|
+
clientNameResolver = resolver;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* HTTP header values must be printable Latin-1 with no control characters;
|
|
29
|
+
* Node's fetch throws (not returns an error, THROWS) on a header value
|
|
30
|
+
* containing anything else, so an MCP client reporting a non-ASCII name
|
|
31
|
+
* (e.g. "通义灵码") or one with an en/em dash would otherwise break every
|
|
32
|
+
* single tool call. Strips anything outside printable ASCII rather than
|
|
33
|
+
* encoding it, since this is just a display label, not data that needs to
|
|
34
|
+
* round-trip losslessly.
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeClientHeaderValue(value) {
|
|
37
|
+
return value.replace(/[^\x20-\x7e]/g, '').trim().slice(0, 200);
|
|
38
|
+
}
|
|
3
39
|
export class PosterlyClient {
|
|
4
40
|
baseUrl;
|
|
5
41
|
apiKey;
|
|
@@ -16,6 +52,15 @@ export class PosterlyClient {
|
|
|
16
52
|
getBaseUrl() {
|
|
17
53
|
return this.baseUrl;
|
|
18
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs `fn` with a fresh per-call tool name/id scoped via AsyncLocalStorage,
|
|
57
|
+
* so any request() calls made inside it (directly or nested) are tagged
|
|
58
|
+
* with X-Posterly-Tool / X-Posterly-Tool-Call. Called once per tool
|
|
59
|
+
* invocation from index.ts's server.tool() handlers.
|
|
60
|
+
*/
|
|
61
|
+
async runToolCall(toolName, fn) {
|
|
62
|
+
return toolCallStorage.run({ toolName, toolCallId: randomUUID() }, fn);
|
|
63
|
+
}
|
|
19
64
|
async probeWhoami(timeoutMs = 2500) {
|
|
20
65
|
const startedAt = Date.now();
|
|
21
66
|
if (!this.apiKey) {
|
|
@@ -78,6 +123,16 @@ export class PosterlyClient {
|
|
|
78
123
|
Authorization: `Bearer ${this.apiKey}`,
|
|
79
124
|
'X-Posterly-Source': 'mcp_stdio',
|
|
80
125
|
};
|
|
126
|
+
const toolCall = toolCallStorage.getStore();
|
|
127
|
+
if (toolCall) {
|
|
128
|
+
headers['X-Posterly-Tool'] = toolCall.toolName;
|
|
129
|
+
headers['X-Posterly-Tool-Call'] = toolCall.toolCallId;
|
|
130
|
+
}
|
|
131
|
+
const clientName = clientNameResolver?.();
|
|
132
|
+
const clientLabel = clientName ? sanitizeClientHeaderValue(clientName) : '';
|
|
133
|
+
if (clientLabel) {
|
|
134
|
+
headers['X-Posterly-Client'] = clientLabel;
|
|
135
|
+
}
|
|
81
136
|
const init = { method, headers };
|
|
82
137
|
if (body instanceof FormData) {
|
|
83
138
|
init.body = body;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview-then-confirm tokens for MCP write actions, shared by both surfaces.
|
|
3
|
+
* Depends on node:crypto only.
|
|
4
|
+
*
|
|
5
|
+
* Format: pv1.<exp>.<nonce>.<mac>
|
|
6
|
+
* exp unix seconds when the preview stops being confirmable
|
|
7
|
+
* nonce random, base64url; also the one-time-use key for nonce actions
|
|
8
|
+
* mac base64url HMAC-SHA256(key,
|
|
9
|
+
* "posterly-mcp-preview-v1|" + exp + "|" + nonce + "|" + userId
|
|
10
|
+
* + "|" + tool + "|" + action + "|" + canonicalJson(args))
|
|
11
|
+
*
|
|
12
|
+
* The token binds the preview to one user, one tool, one action, and the
|
|
13
|
+
* exact (normalised) arguments, so a confirm call with different args, from a
|
|
14
|
+
* different user, or after expiry is refused. It is UX protection that a
|
|
15
|
+
* human saw what will change, not authorization: REST scopes and ownership
|
|
16
|
+
* checks still decide what is allowed.
|
|
17
|
+
*
|
|
18
|
+
* Callers own the key (hosted: derived from a server secret; stdio: random
|
|
19
|
+
* per process) and the one-time-use store (hosted: Redis; stdio: memory).
|
|
20
|
+
*/
|
|
21
|
+
export declare const PREVIEW_TOKEN_VERSION = "pv1";
|
|
22
|
+
export interface PreviewBinding {
|
|
23
|
+
userId: string;
|
|
24
|
+
tool: string;
|
|
25
|
+
action: string;
|
|
26
|
+
/** Arguments AFTER validation/normalisation (defaults applied, numbers coerced). */
|
|
27
|
+
args: unknown;
|
|
28
|
+
}
|
|
29
|
+
export type VerifyPreviewResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
exp: number;
|
|
32
|
+
nonce: string;
|
|
33
|
+
remainingSeconds: number;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
reason: 'malformed' | 'expired' | 'mismatch';
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Canonical JSON of a call's arguments: `confirm` and `preview_id` removed
|
|
40
|
+
* (top level), undefined dropped, keys sorted recursively, arrays kept in
|
|
41
|
+
* order, no whitespace. Same rules as canonicalArgs in tool-registry.ts.
|
|
42
|
+
*/
|
|
43
|
+
export declare function canonicalJson(args: unknown): string;
|
|
44
|
+
/** Derives the MAC key from a server secret with a domain prefix, so the raw secret is never the key. */
|
|
45
|
+
export declare function derivePreviewKey(secret: string | Buffer, domain?: string): Buffer;
|
|
46
|
+
export declare function createPreviewToken(key: Buffer, binding: PreviewBinding, options: {
|
|
47
|
+
ttlSeconds: number;
|
|
48
|
+
nowMs?: number;
|
|
49
|
+
nonce?: string;
|
|
50
|
+
}): string;
|
|
51
|
+
export declare function parsePreviewToken(token: unknown): {
|
|
52
|
+
exp: number;
|
|
53
|
+
nonce: string;
|
|
54
|
+
mac: string;
|
|
55
|
+
} | null;
|
|
56
|
+
export declare function verifyPreviewToken(key: Buffer, token: unknown, binding: PreviewBinding, options?: {
|
|
57
|
+
nowMs?: number;
|
|
58
|
+
}): VerifyPreviewResult;
|
|
59
|
+
/** Stable fingerprint of a token for one-time-use bookkeeping (never store the raw token). */
|
|
60
|
+
export declare function previewTokenFingerprint(token: string): string;
|
|
61
|
+
/** Plain-language refusal for each verification failure. */
|
|
62
|
+
export declare function previewFailureMessage(reason: 'malformed' | 'expired' | 'mismatch' | 'used' | 'missing' | 'unavailable'): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview-then-confirm tokens for MCP write actions, shared by both surfaces.
|
|
3
|
+
* Depends on node:crypto only.
|
|
4
|
+
*
|
|
5
|
+
* Format: pv1.<exp>.<nonce>.<mac>
|
|
6
|
+
* exp unix seconds when the preview stops being confirmable
|
|
7
|
+
* nonce random, base64url; also the one-time-use key for nonce actions
|
|
8
|
+
* mac base64url HMAC-SHA256(key,
|
|
9
|
+
* "posterly-mcp-preview-v1|" + exp + "|" + nonce + "|" + userId
|
|
10
|
+
* + "|" + tool + "|" + action + "|" + canonicalJson(args))
|
|
11
|
+
*
|
|
12
|
+
* The token binds the preview to one user, one tool, one action, and the
|
|
13
|
+
* exact (normalised) arguments, so a confirm call with different args, from a
|
|
14
|
+
* different user, or after expiry is refused. It is UX protection that a
|
|
15
|
+
* human saw what will change, not authorization: REST scopes and ownership
|
|
16
|
+
* checks still decide what is allowed.
|
|
17
|
+
*
|
|
18
|
+
* Callers own the key (hosted: derived from a server secret; stdio: random
|
|
19
|
+
* per process) and the one-time-use store (hosted: Redis; stdio: memory).
|
|
20
|
+
*/
|
|
21
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
22
|
+
export const PREVIEW_TOKEN_VERSION = 'pv1';
|
|
23
|
+
const MAC_DOMAIN = 'posterly-mcp-preview-v1';
|
|
24
|
+
function isPlainObject(value) {
|
|
25
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function sortKeysDeep(value) {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map((item) => (item === undefined ? null : sortKeysDeep(item)));
|
|
30
|
+
if (isPlainObject(value)) {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const key of Object.keys(value).sort()) {
|
|
33
|
+
const item = value[key];
|
|
34
|
+
if (item === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
out[key] = sortKeysDeep(item);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Canonical JSON of a call's arguments: `confirm` and `preview_id` removed
|
|
44
|
+
* (top level), undefined dropped, keys sorted recursively, arrays kept in
|
|
45
|
+
* order, no whitespace. Same rules as canonicalArgs in tool-registry.ts.
|
|
46
|
+
*/
|
|
47
|
+
export function canonicalJson(args) {
|
|
48
|
+
const source = isPlainObject(args) ? args : {};
|
|
49
|
+
const stripped = {};
|
|
50
|
+
for (const [key, value] of Object.entries(source)) {
|
|
51
|
+
if (key === 'confirm' || key === 'preview_id')
|
|
52
|
+
continue;
|
|
53
|
+
stripped[key] = value;
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(sortKeysDeep(stripped));
|
|
56
|
+
}
|
|
57
|
+
/** Derives the MAC key from a server secret with a domain prefix, so the raw secret is never the key. */
|
|
58
|
+
export function derivePreviewKey(secret, domain = 'posterly-mcp-preview-key-v1') {
|
|
59
|
+
return createHmac('sha256', secret).update(domain).digest();
|
|
60
|
+
}
|
|
61
|
+
function field(value) {
|
|
62
|
+
// Pipes separate fields; ids never contain them, but never let one shift a
|
|
63
|
+
// boundary. Percent is escaped first so the encoding is injective: a value
|
|
64
|
+
// that already contains "%7C" cannot collide with one that contains "|".
|
|
65
|
+
return value.replace(/%/g, '%25').replace(/\|/g, '%7C');
|
|
66
|
+
}
|
|
67
|
+
function computeMac(key, exp, nonce, binding) {
|
|
68
|
+
const payload = [
|
|
69
|
+
MAC_DOMAIN,
|
|
70
|
+
String(exp),
|
|
71
|
+
nonce,
|
|
72
|
+
field(binding.userId),
|
|
73
|
+
field(binding.tool),
|
|
74
|
+
field(binding.action),
|
|
75
|
+
canonicalJson(binding.args),
|
|
76
|
+
].join('|');
|
|
77
|
+
return createHmac('sha256', key).update(payload).digest('base64url');
|
|
78
|
+
}
|
|
79
|
+
export function createPreviewToken(key, binding, options) {
|
|
80
|
+
const now = Math.floor((options.nowMs ?? Date.now()) / 1000);
|
|
81
|
+
const exp = now + Math.max(1, Math.floor(options.ttlSeconds));
|
|
82
|
+
const nonce = options.nonce ?? randomBytes(16).toString('base64url');
|
|
83
|
+
const mac = computeMac(key, exp, nonce, binding);
|
|
84
|
+
return `${PREVIEW_TOKEN_VERSION}.${exp}.${nonce}.${mac}`;
|
|
85
|
+
}
|
|
86
|
+
export function parsePreviewToken(token) {
|
|
87
|
+
if (typeof token !== 'string')
|
|
88
|
+
return null;
|
|
89
|
+
const parts = token.split('.');
|
|
90
|
+
if (parts.length !== 4 || parts[0] !== PREVIEW_TOKEN_VERSION)
|
|
91
|
+
return null;
|
|
92
|
+
const [, expRaw, nonce, mac] = parts;
|
|
93
|
+
if (!/^\d{1,12}$/.test(expRaw) || !/^[A-Za-z0-9_-]{8,64}$/.test(nonce) || !/^[A-Za-z0-9_-]{20,100}$/.test(mac))
|
|
94
|
+
return null;
|
|
95
|
+
return { exp: Number(expRaw), nonce, mac };
|
|
96
|
+
}
|
|
97
|
+
export function verifyPreviewToken(key, token, binding, options = {}) {
|
|
98
|
+
const parsed = parsePreviewToken(token);
|
|
99
|
+
if (!parsed)
|
|
100
|
+
return { ok: false, reason: 'malformed' };
|
|
101
|
+
const expected = Buffer.from(computeMac(key, parsed.exp, parsed.nonce, binding));
|
|
102
|
+
const actual = Buffer.from(parsed.mac);
|
|
103
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
104
|
+
return { ok: false, reason: 'mismatch' };
|
|
105
|
+
}
|
|
106
|
+
const now = Math.floor((options.nowMs ?? Date.now()) / 1000);
|
|
107
|
+
if (parsed.exp <= now)
|
|
108
|
+
return { ok: false, reason: 'expired' };
|
|
109
|
+
return { ok: true, exp: parsed.exp, nonce: parsed.nonce, remainingSeconds: parsed.exp - now };
|
|
110
|
+
}
|
|
111
|
+
/** Stable fingerprint of a token for one-time-use bookkeeping (never store the raw token). */
|
|
112
|
+
export function previewTokenFingerprint(token) {
|
|
113
|
+
return createHash('sha256').update(token).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
/** Plain-language refusal for each verification failure. */
|
|
116
|
+
export function previewFailureMessage(reason) {
|
|
117
|
+
switch (reason) {
|
|
118
|
+
case 'missing':
|
|
119
|
+
return 'confirm: true needs the preview_id from a preview call. Call once without confirm, show the preview to the user, then confirm with its preview_id.';
|
|
120
|
+
case 'expired':
|
|
121
|
+
return 'This preview expired, so nothing was changed. Call again without confirm to get a fresh preview.';
|
|
122
|
+
case 'mismatch':
|
|
123
|
+
return 'This preview does not match these arguments, so nothing was changed. Call again without confirm to get a fresh preview for exactly what should happen.';
|
|
124
|
+
case 'used':
|
|
125
|
+
return 'This preview was already used, so nothing was changed. Call again without confirm to get a fresh preview.';
|
|
126
|
+
case 'unavailable':
|
|
127
|
+
return 'posterly could not check this preview right now, so nothing was changed. Please retry in a moment.';
|
|
128
|
+
default:
|
|
129
|
+
return 'preview_id is not valid, so nothing was changed. Call again without confirm to get a fresh preview.';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio (npm package) preview tokens: a random key per process and an
|
|
3
|
+
* in-memory one-time-use set. A preview only survives as long as the local
|
|
4
|
+
* MCP process does, which matches how a local client uses it (preview and
|
|
5
|
+
* confirm happen in the same session). This is UX protection only: REST
|
|
6
|
+
* authorization on the posterly API is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import { type PreviewBinding, type VerifyPreviewResult } from './preview-token-core.js';
|
|
9
|
+
export declare const STDIO_PREVIEW_TTL_SECONDS: number;
|
|
10
|
+
export declare class StdioPreviewTokens {
|
|
11
|
+
private readonly ttlSeconds;
|
|
12
|
+
private readonly key;
|
|
13
|
+
private readonly used;
|
|
14
|
+
constructor(key?: Buffer, ttlSeconds?: number);
|
|
15
|
+
create(binding: PreviewBinding, nowMs?: number): string;
|
|
16
|
+
verify(token: unknown, binding: PreviewBinding, nowMs?: number): VerifyPreviewResult;
|
|
17
|
+
/**
|
|
18
|
+
* Marks a verified token as used. Returns false when it was already used
|
|
19
|
+
* (a second confirm with the same preview_id). Call only after verify().
|
|
20
|
+
*/
|
|
21
|
+
claimOnce(token: string, expSeconds: number, nowMs?: number): boolean;
|
|
22
|
+
private sweep;
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio (npm package) preview tokens: a random key per process and an
|
|
3
|
+
* in-memory one-time-use set. A preview only survives as long as the local
|
|
4
|
+
* MCP process does, which matches how a local client uses it (preview and
|
|
5
|
+
* confirm happen in the same session). This is UX protection only: REST
|
|
6
|
+
* authorization on the posterly API is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { createPreviewToken, previewTokenFingerprint, verifyPreviewToken, } from './preview-token-core.js';
|
|
10
|
+
export const STDIO_PREVIEW_TTL_SECONDS = 10 * 60;
|
|
11
|
+
export class StdioPreviewTokens {
|
|
12
|
+
ttlSeconds;
|
|
13
|
+
key;
|
|
14
|
+
used = new Map();
|
|
15
|
+
constructor(key = randomBytes(32), ttlSeconds = STDIO_PREVIEW_TTL_SECONDS) {
|
|
16
|
+
this.ttlSeconds = ttlSeconds;
|
|
17
|
+
this.key = key;
|
|
18
|
+
}
|
|
19
|
+
create(binding, nowMs) {
|
|
20
|
+
return createPreviewToken(this.key, binding, { ttlSeconds: this.ttlSeconds, nowMs });
|
|
21
|
+
}
|
|
22
|
+
verify(token, binding, nowMs) {
|
|
23
|
+
return verifyPreviewToken(this.key, token, binding, { nowMs });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Marks a verified token as used. Returns false when it was already used
|
|
27
|
+
* (a second confirm with the same preview_id). Call only after verify().
|
|
28
|
+
*/
|
|
29
|
+
claimOnce(token, expSeconds, nowMs = Date.now()) {
|
|
30
|
+
this.sweep(nowMs);
|
|
31
|
+
const fingerprint = previewTokenFingerprint(token);
|
|
32
|
+
if (this.used.has(fingerprint))
|
|
33
|
+
return false;
|
|
34
|
+
this.used.set(fingerprint, expSeconds);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
sweep(nowMs) {
|
|
38
|
+
const now = Math.floor(nowMs / 1000);
|
|
39
|
+
for (const [fingerprint, exp] of this.used) {
|
|
40
|
+
if (exp <= now)
|
|
41
|
+
this.used.delete(fingerprint);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|