@frontmcp/skills 1.1.2-beta.1 → 1.2.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/catalog/TEMPLATE.md +16 -11
- package/catalog/frontmcp-authorities/SKILL.md +116 -11
- package/catalog/frontmcp-authorities/references/authority-profiles.md +39 -36
- package/catalog/frontmcp-authorities/references/claims-mapping.md +7 -0
- package/catalog/frontmcp-authorities/references/custom-evaluators.md +63 -14
- package/catalog/frontmcp-channels/SKILL.md +36 -0
- package/catalog/frontmcp-channels/examples/channel-sources/file-watcher.md +8 -2
- package/catalog/frontmcp-channels/examples/channel-sources/replay-buffer.md +111 -30
- package/catalog/frontmcp-channels/examples/channel-two-way/whatsapp-bridge.md +45 -3
- package/catalog/frontmcp-channels/references/channel-sources.md +11 -3
- package/catalog/frontmcp-channels/references/channel-two-way.md +60 -89
- package/catalog/frontmcp-config/SKILL.md +111 -8
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +4 -4
- package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +7 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +1 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/json-schema-ide-support.md +1 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/multi-target-with-security.md +12 -9
- package/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md +2 -2
- package/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md +1 -1
- package/catalog/frontmcp-config/examples/configure-security-headers/csp-report-only.md +1 -1
- package/catalog/frontmcp-config/examples/configure-security-headers/full-production-headers.md +1 -1
- package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-basic.md +76 -0
- package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +116 -0
- package/catalog/frontmcp-config/examples/configure-skills-http/inject-instructions.md +59 -0
- package/catalog/frontmcp-config/references/configure-auth-modes.md +5 -5
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +27 -24
- package/catalog/frontmcp-config/references/configure-http.md +14 -10
- package/catalog/frontmcp-config/references/configure-security-headers.md +2 -2
- package/catalog/frontmcp-config/references/configure-session.md +25 -25
- package/catalog/frontmcp-config/references/configure-skills-http.md +157 -0
- package/catalog/frontmcp-config/references/configure-throttle.md +1 -1
- package/catalog/frontmcp-config/references/configure-transport.md +2 -2
- package/catalog/frontmcp-deployment/SKILL.md +112 -9
- package/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md +23 -11
- package/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md +44 -17
- package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +53 -21
- package/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md +1 -1
- package/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md +1 -1
- package/catalog/frontmcp-deployment/examples/build-for-mcpb/mcpb-bundle-build.md +1 -1
- package/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md +1 -1
- package/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md +7 -8
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md +8 -6
- package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md +5 -4
- package/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md +8 -5
- package/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md +20 -18
- package/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md +8 -5
- package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +3 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md +2 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md +2 -2
- package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md +23 -21
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +25 -22
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md +23 -30
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md +52 -28
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md +32 -55
- package/catalog/frontmcp-deployment/examples/mcp-client-integration/http-remote.md +9 -0
- package/catalog/frontmcp-deployment/references/build-for-browser.md +40 -17
- package/catalog/frontmcp-deployment/references/build-for-cli.md +8 -8
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +43 -24
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +36 -25
- package/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md +56 -14
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +9 -6
- package/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +57 -58
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +49 -59
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +2 -0
- package/catalog/frontmcp-development/SKILL.md +186 -11
- package/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md +1 -1
- package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +30 -27
- package/catalog/frontmcp-development/examples/create-job/job-with-permissions.md +13 -8
- package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +33 -23
- package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +19 -10
- package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +3 -3
- package/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md +6 -4
- package/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md +1 -1
- package/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md +13 -8
- package/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md +50 -23
- package/catalog/frontmcp-development/references/create-agent.md +47 -30
- package/catalog/frontmcp-development/references/create-job.md +69 -54
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +45 -28
- package/catalog/frontmcp-development/references/create-plugin.md +10 -8
- package/catalog/frontmcp-development/references/create-prompt.md +3 -3
- package/catalog/frontmcp-development/references/create-provider.md +91 -51
- package/catalog/frontmcp-development/references/create-resource.md +3 -3
- package/catalog/frontmcp-development/references/create-skill.md +2 -2
- package/catalog/frontmcp-development/references/create-tool.md +7 -7
- package/catalog/frontmcp-development/references/create-workflow.md +8 -10
- package/catalog/frontmcp-development/references/decorators-guide.md +92 -56
- package/catalog/frontmcp-development/references/official-plugins.md +4 -3
- package/catalog/frontmcp-development/references/openapi-adapter.md +1 -1
- package/catalog/frontmcp-extensibility/SKILL.md +70 -10
- package/catalog/frontmcp-extensibility/examples/skill-audit-log/custom-store.md +197 -0
- package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +68 -0
- package/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md +3 -5
- package/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md +4 -11
- package/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md +41 -30
- package/catalog/frontmcp-extensibility/references/skill-audit-log.md +233 -0
- package/catalog/frontmcp-extensibility/references/vectoriadb.md +73 -63
- package/catalog/frontmcp-guides/SKILL.md +84 -27
- package/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md +72 -62
- package/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md +32 -43
- package/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md +24 -17
- package/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md +23 -21
- package/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md +47 -39
- package/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md +16 -6
- package/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md +9 -8
- package/catalog/frontmcp-guides/references/example-knowledge-base.md +192 -265
- package/catalog/frontmcp-guides/references/example-task-manager.md +60 -54
- package/catalog/frontmcp-guides/references/example-weather-api.md +22 -24
- package/catalog/frontmcp-observability/SKILL.md +66 -2
- package/catalog/frontmcp-observability/examples/telemetry-api/skill-counters.md +100 -0
- package/catalog/frontmcp-observability/examples/tracing-setup/production-tracing.md +7 -2
- package/catalog/frontmcp-observability/examples/vendor-integrations/coralogix-setup.md +6 -2
- package/catalog/frontmcp-observability/references/telemetry-api.md +72 -8
- package/catalog/frontmcp-observability/references/testing-observability.md +33 -49
- package/catalog/frontmcp-observability/references/tracing-setup.md +12 -5
- package/catalog/frontmcp-observability/references/vendor-integrations.md +46 -1
- package/catalog/frontmcp-production-readiness/SKILL.md +134 -3
- package/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md +57 -36
- package/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md +1 -1
- package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +102 -6
- package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/daemon-socket-config.md +2 -1
- package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/graceful-shutdown-cleanup.md +66 -58
- package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/security-and-permissions.md +5 -3
- package/catalog/frontmcp-production-readiness/examples/production-cloudflare/durable-objects-state.md +2 -1
- package/catalog/frontmcp-production-readiness/examples/production-cloudflare/wrangler-config.md +55 -76
- package/catalog/frontmcp-production-readiness/examples/production-lambda/cold-start-connection-reuse.md +43 -40
- package/catalog/frontmcp-production-readiness/examples/production-lambda/sam-template.md +63 -94
- package/catalog/frontmcp-production-readiness/examples/production-lambda/scaling-and-monitoring.md +28 -18
- package/catalog/frontmcp-production-readiness/examples/production-node-sdk/multi-instance-cleanup.md +29 -14
- package/catalog/frontmcp-production-readiness/examples/production-node-server/graceful-shutdown.md +58 -42
- package/catalog/frontmcp-production-readiness/examples/production-node-server/redis-session-scaling.md +5 -2
- package/catalog/frontmcp-production-readiness/examples/production-vercel/cold-start-optimization.md +41 -24
- package/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md +56 -65
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +17 -5
- package/catalog/frontmcp-production-readiness/references/production-cli-daemon.md +5 -5
- package/catalog/frontmcp-production-readiness/references/production-cloudflare.md +5 -5
- package/catalog/frontmcp-production-readiness/references/production-lambda.md +5 -5
- package/catalog/frontmcp-production-readiness/references/production-node-sdk.md +5 -5
- package/catalog/frontmcp-production-readiness/references/production-node-server.md +1 -1
- package/catalog/frontmcp-production-readiness/references/production-vercel.md +5 -5
- package/catalog/frontmcp-setup/SKILL.md +88 -0
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-workspace-with-apps.md +10 -4
- package/catalog/frontmcp-setup/examples/project-structure-standalone/dev-workflow-commands.md +21 -8
- package/catalog/frontmcp-setup/examples/readme-guide/node-server-readme.md +3 -3
- package/catalog/frontmcp-setup/references/multi-app-composition.md +4 -3
- package/catalog/frontmcp-setup/references/project-structure-nx.md +15 -6
- package/catalog/frontmcp-setup/references/project-structure-standalone.md +18 -15
- package/catalog/frontmcp-setup/references/readme-guide.md +1 -1
- package/catalog/frontmcp-setup/references/setup-project.md +19 -5
- package/catalog/frontmcp-setup/references/setup-redis.md +27 -39
- package/catalog/frontmcp-setup/references/setup-sqlite.md +25 -18
- package/catalog/frontmcp-testing/SKILL.md +102 -15
- package/catalog/frontmcp-testing/examples/setup-testing/unit-test-tool-resource-prompt.md +3 -3
- package/catalog/frontmcp-testing/examples/test-auth/oauth-flow-test.md +50 -39
- package/catalog/frontmcp-testing/examples/test-auth/role-based-access-test.md +52 -29
- package/catalog/frontmcp-testing/examples/test-auth/token-factory-test.md +37 -20
- package/catalog/frontmcp-testing/examples/test-direct-client/basic-create-test.md +25 -15
- package/catalog/frontmcp-testing/examples/test-direct-client/openai-claude-format-test.md +27 -21
- package/catalog/frontmcp-testing/examples/test-e2e-handler/basic-e2e-test.md +29 -20
- package/catalog/frontmcp-testing/examples/test-e2e-handler/manual-client-with-transport.md +5 -3
- package/catalog/frontmcp-testing/examples/test-e2e-handler/tool-call-and-error-e2e.md +35 -26
- package/catalog/frontmcp-testing/examples/test-tool-unit/basic-tool-test.md +8 -3
- package/catalog/frontmcp-testing/examples/test-tool-unit/schema-validation-test.md +4 -1
- package/catalog/frontmcp-testing/examples/test-tool-unit/tool-error-handling-test.md +6 -3
- package/catalog/frontmcp-testing/references/setup-testing.md +35 -39
- package/catalog/frontmcp-testing/references/test-auth.md +86 -43
- package/catalog/frontmcp-testing/references/test-browser-build.md +1 -1
- package/catalog/frontmcp-testing/references/test-direct-client.md +29 -19
- package/catalog/frontmcp-testing/references/test-e2e-handler.md +31 -19
- package/catalog/frontmcp-testing/references/test-tool-unit.md +6 -2
- package/catalog/skills-manifest.json +428 -339
- package/package.json +1 -1
- package/src/manifest.d.ts +13 -0
- package/src/manifest.js.map +1 -1
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
name: replay-buffer
|
|
3
3
|
reference: channel-sources
|
|
4
4
|
level: advanced
|
|
5
|
-
description: Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline
|
|
6
|
-
tags:
|
|
5
|
+
description: Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline.
|
|
6
|
+
tags:
|
|
7
|
+
- replay
|
|
8
|
+
- buffer
|
|
9
|
+
- persistence
|
|
10
|
+
- offline
|
|
11
|
+
- reconnect
|
|
7
12
|
features:
|
|
8
|
-
- Replay configuration with maxEvents cap
|
|
13
|
+
- Replay configuration with `maxEvents` cap
|
|
9
14
|
- In-memory ring buffer for event storage
|
|
10
|
-
-
|
|
11
|
-
- Persistent store pattern with onConnect
|
|
15
|
+
- Where `replayBufferedEvents` actually lives (on `ChannelInstance`, not on `ChannelContext`)
|
|
16
|
+
- Persistent store pattern with `onConnect` and a user-defined Redis provider
|
|
12
17
|
---
|
|
13
18
|
|
|
14
19
|
# Replay Buffer Pattern
|
|
15
20
|
|
|
16
|
-
Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline
|
|
21
|
+
Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline.
|
|
17
22
|
|
|
18
23
|
## Basic Replay (In-Memory)
|
|
19
24
|
|
|
@@ -38,42 +43,107 @@ class CIAlertChannel extends ChannelContext {
|
|
|
38
43
|
}
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
The SDK keeps a per-channel ring buffer of the last `maxEvents` notifications. Replay is delivered via `ChannelInstance.replayBufferedEvents(sessionId)` (defined in `libs/sdk/src/channel/channel.instance.ts:223`). That method lives on the **channel instance**, not on `ChannelContext`, so you cannot call `this.replayBufferedEvents(...)` from within `onEvent`/`onConnect`.
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
Replay is **not** triggered automatically when a new session subscribes — there is no caller of `replayBufferedEvents` inside the SDK today. To deliver buffered events, expose a tool that resolves the channel from the registry and triggers replay explicitly (Claude Code can call this on demand, or your application can call it from a custom hook):
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
```typescript
|
|
51
|
+
// src/apps/alerts/tools/replay-ci-alerts.tool.ts
|
|
52
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
53
|
+
import type ChannelRegistry from '@frontmcp/sdk/channel/channel.registry';
|
|
54
|
+
|
|
55
|
+
@Tool({
|
|
56
|
+
name: 'replay-ci-alerts',
|
|
57
|
+
description: 'Replay buffered CI alerts to the current session.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
channel_name: z.string().default('ci-alerts'),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
export class ReplayCIAlertsTool extends ToolContext {
|
|
63
|
+
async execute(input) {
|
|
64
|
+
// Resolve the channel registry off scope (same cast pattern as ChannelReplyTool)
|
|
65
|
+
const scope = this.scope as unknown as { channels?: ChannelRegistry };
|
|
66
|
+
const registry = scope.channels;
|
|
67
|
+
if (!registry) {
|
|
68
|
+
return { content: [{ type: 'text', text: 'Channels are not enabled on this server.' }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const channel = registry.findByName(input.channel_name);
|
|
72
|
+
if (!channel) {
|
|
73
|
+
return { content: [{ type: 'text', text: `Channel "${input.channel_name}" not found.` }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sessionId = this.context.sessionId;
|
|
77
|
+
const replayed = channel.replayBufferedEvents(sessionId);
|
|
78
|
+
return { content: [{ type: 'text', text: `Replayed ${replayed} buffered event(s).` }] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Replayed events arrive at Claude Code with `replayed: "true"` in their meta so the model can distinguish them from live events.
|
|
84
|
+
|
|
85
|
+
## Persistent Store Pattern (Redis)
|
|
86
|
+
|
|
87
|
+
The in-memory ring buffer does not survive process restarts. To persist events across restarts, push events into Redis and rehydrate on `onConnect()`. Inject a Redis-backed provider the same way the demo apps do (`apps/demo/src/apps/employee-time/providers/redis.provider.ts`):
|
|
46
88
|
|
|
47
89
|
```typescript
|
|
90
|
+
// src/apps/alerts/providers/alerts-redis.provider.ts
|
|
91
|
+
import Redis, { Redis as RedisClient } from 'ioredis';
|
|
92
|
+
|
|
93
|
+
import { AsyncProvider, ProviderScope } from '@frontmcp/sdk';
|
|
94
|
+
|
|
95
|
+
export default class AlertsRedisProvider {
|
|
96
|
+
readonly client: RedisClient;
|
|
97
|
+
|
|
98
|
+
constructor() {
|
|
99
|
+
this.client = new Redis({ host: 'localhost', port: 6379 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const createAlertsRedisProvider = AsyncProvider({
|
|
104
|
+
provide: AlertsRedisProvider,
|
|
105
|
+
name: 'AlertsRedisProvider',
|
|
106
|
+
scope: ProviderScope.GLOBAL,
|
|
107
|
+
inject: () => [] as const,
|
|
108
|
+
useFactory: async () => new AlertsRedisProvider(),
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Resolve the provider inside the channel via `this.get(AlertsRedisProvider)` and use it to load + record events:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// src/apps/alerts/channels/persistent-alert.channel.ts
|
|
116
|
+
import { Channel, ChannelContext, type ChannelNotification } from '@frontmcp/sdk';
|
|
117
|
+
|
|
118
|
+
import AlertsRedisProvider from '../providers/alerts-redis.provider';
|
|
119
|
+
|
|
48
120
|
@Channel({
|
|
49
121
|
name: 'alerts',
|
|
50
122
|
description: 'Persistent alerts with Redis-backed replay',
|
|
51
123
|
source: { type: 'service', service: 'alert-store' },
|
|
52
124
|
replay: { enabled: true, maxEvents: 500 },
|
|
53
125
|
})
|
|
54
|
-
class PersistentAlertChannel extends ChannelContext {
|
|
126
|
+
export class PersistentAlertChannel extends ChannelContext {
|
|
55
127
|
async onConnect(): Promise<void> {
|
|
56
|
-
|
|
57
|
-
const redis = this.get(RedisClientToken);
|
|
58
|
-
const stored = await redis.lrange('channel:alerts:buffer', 0, -1);
|
|
128
|
+
const redis = this.get(AlertsRedisProvider).client;
|
|
59
129
|
|
|
130
|
+
// Replay history from Redis into the channel pipeline
|
|
131
|
+
const stored = await redis.lrange('channel:alerts:buffer', 0, -1);
|
|
60
132
|
for (const raw of stored) {
|
|
61
133
|
const event = JSON.parse(raw);
|
|
62
134
|
this.pushIncoming(event);
|
|
63
135
|
}
|
|
64
136
|
|
|
65
|
-
// Subscribe to new events
|
|
137
|
+
// Subscribe to new events and persist them as they arrive
|
|
66
138
|
const subscriber = redis.duplicate();
|
|
67
|
-
await subscriber.subscribe('channel:alerts'
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
redis.
|
|
71
|
-
|
|
72
|
-
// Push to connected Claude sessions
|
|
73
|
-
this.pushIncoming(event);
|
|
139
|
+
await subscriber.subscribe('channel:alerts');
|
|
140
|
+
subscriber.on('message', async (_channel, message) => {
|
|
141
|
+
await redis.rpush('channel:alerts:buffer', message);
|
|
142
|
+
await redis.ltrim('channel:alerts:buffer', -500, -1);
|
|
143
|
+
this.pushIncoming(JSON.parse(message));
|
|
74
144
|
});
|
|
75
145
|
|
|
76
|
-
this.logger.info('Alert store connected, loaded
|
|
146
|
+
this.logger.info('Alert store connected, history loaded');
|
|
77
147
|
}
|
|
78
148
|
|
|
79
149
|
async onEvent(payload: unknown): Promise<ChannelNotification> {
|
|
@@ -86,20 +156,31 @@ class PersistentAlertChannel extends ChannelContext {
|
|
|
86
156
|
}
|
|
87
157
|
```
|
|
88
158
|
|
|
159
|
+
Register the provider on the app so DI can resolve it:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
@App({
|
|
163
|
+
name: 'Alerts',
|
|
164
|
+
channels: [PersistentAlertChannel],
|
|
165
|
+
providers: [createAlertsRedisProvider],
|
|
166
|
+
})
|
|
167
|
+
class AlertsApp {}
|
|
168
|
+
```
|
|
169
|
+
|
|
89
170
|
## How Replay Works
|
|
90
171
|
|
|
91
|
-
1. Events arrive via any source
|
|
92
|
-
2. If `replay.enabled`,
|
|
93
|
-
3.
|
|
94
|
-
4.
|
|
95
|
-
5.
|
|
172
|
+
1. Events arrive via any source -> `onEvent()` transforms them -> notification pushed to live sessions.
|
|
173
|
+
2. If `replay.enabled`, each pushed notification is also stored in the channel's ring buffer (FIFO, capped at `maxEvents`).
|
|
174
|
+
3. Replay is **user-triggered**: call `ChannelInstance.replayBufferedEvents(sessionId)` yourself — typically from a tool (see `ReplayCIAlertsTool` above) or from a custom session lifecycle hook in your app.
|
|
175
|
+
4. Each buffered notification is sent to the target session with `replayed: "true"` injected into its meta.
|
|
176
|
+
5. The persistent-store pattern shown above is independent of the in-memory buffer: you replay from Redis on `onConnect()` (which fires once when the channel boots), while the in-memory ring buffer is what `replayBufferedEvents` reads from when invoked.
|
|
96
177
|
|
|
97
178
|
## What This Demonstrates
|
|
98
179
|
|
|
99
|
-
- Replay configuration with maxEvents cap
|
|
180
|
+
- Replay configuration with `maxEvents` cap
|
|
100
181
|
- In-memory ring buffer for event storage
|
|
101
|
-
-
|
|
102
|
-
- Persistent store pattern with onConnect
|
|
182
|
+
- Where `replayBufferedEvents` actually lives (on `ChannelInstance`, not on `ChannelContext`)
|
|
183
|
+
- Persistent store pattern with `onConnect` and a user-defined Redis provider
|
|
103
184
|
|
|
104
185
|
## Related
|
|
105
186
|
|
|
@@ -20,9 +20,30 @@ Full WhatsApp Business API bridge allowing users to chat with Claude Code via Wh
|
|
|
20
20
|
```typescript
|
|
21
21
|
// src/apps/messaging/channels/whatsapp.channel.ts
|
|
22
22
|
import { Channel, ChannelContext, ChannelNotification } from '@frontmcp/sdk';
|
|
23
|
+
import { hmacSha256 } from '@frontmcp/utils';
|
|
23
24
|
|
|
24
25
|
const ALLOWED_SENDERS = new Set((process.env['WHATSAPP_ALLOWED_SENDERS'] ?? '').split(',').filter(Boolean));
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Verify the X-Hub-Signature-256 header WhatsApp Cloud API attaches to every
|
|
29
|
+
* webhook delivery. The header is `sha256=<hex>` where the digest is HMAC-SHA256
|
|
30
|
+
* of the raw request body keyed by the app secret. Uses @frontmcp/utils so the
|
|
31
|
+
* same code runs in Node and Edge runtimes.
|
|
32
|
+
*/
|
|
33
|
+
function verifyWhatsAppSignature(rawBody: string, header: string | undefined, appSecret: string): boolean {
|
|
34
|
+
if (!header?.startsWith('sha256=')) return false;
|
|
35
|
+
const expected = header.slice('sha256='.length);
|
|
36
|
+
|
|
37
|
+
const digest = hmacSha256(new TextEncoder().encode(appSecret), new TextEncoder().encode(rawBody));
|
|
38
|
+
const actual = Array.from(digest, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
39
|
+
|
|
40
|
+
// Constant-time compare to avoid timing leaks
|
|
41
|
+
if (actual.length !== expected.length) return false;
|
|
42
|
+
let diff = 0;
|
|
43
|
+
for (let i = 0; i < actual.length; i++) diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
44
|
+
return diff === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
@Channel({
|
|
27
48
|
name: 'whatsapp',
|
|
28
49
|
description: 'WhatsApp chat bridge - verified users can message Claude Code',
|
|
@@ -32,7 +53,27 @@ const ALLOWED_SENDERS = new Set((process.env['WHATSAPP_ALLOWED_SENDERS'] ?? '').
|
|
|
32
53
|
})
|
|
33
54
|
export class WhatsAppChannel extends ChannelContext {
|
|
34
55
|
async onEvent(payload: unknown): Promise<ChannelNotification> {
|
|
35
|
-
const { body } = payload as {
|
|
56
|
+
const { body, headers } = payload as {
|
|
57
|
+
body: Record<string, unknown>;
|
|
58
|
+
headers: Record<string, string | string[] | undefined>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 1. Verify webhook signature before trusting anything in the payload.
|
|
62
|
+
//
|
|
63
|
+
// NOTE: Meta's X-Hub-Signature-256 is computed over the EXACT raw HTTP body bytes.
|
|
64
|
+
// FrontMCP's `WebhookPayload.body` is the parsed JSON, so re-serializing here is
|
|
65
|
+
// approximate — re-stringification can mismatch when Meta's payload contains
|
|
66
|
+
// characters Meta encoded as `\uXXXX` escapes. For production, capture the raw
|
|
67
|
+
// body with a transport-level middleware (e.g. an Express `verify` hook on
|
|
68
|
+
// `express.json()`) and pass that string into `verifyWhatsAppSignature` instead.
|
|
69
|
+
const sigHeader = headers['x-hub-signature-256'];
|
|
70
|
+
const sig = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
|
|
71
|
+
const appSecret = process.env['WHATSAPP_APP_SECRET'];
|
|
72
|
+
if (!appSecret || !verifyWhatsAppSignature(JSON.stringify(body), sig, appSecret)) {
|
|
73
|
+
this.logger.warn('WhatsApp: signature mismatch, dropping payload');
|
|
74
|
+
return { content: '', meta: { verified: 'false', reason: 'bad_signature' } };
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
const entry = (body as any).entry?.[0];
|
|
37
78
|
const change = entry?.changes?.[0];
|
|
38
79
|
const message = change?.value?.messages?.[0];
|
|
@@ -96,7 +137,8 @@ export class WhatsAppChannel extends ChannelContext {
|
|
|
96
137
|
|
|
97
138
|
```typescript
|
|
98
139
|
// src/main.ts
|
|
99
|
-
import {
|
|
140
|
+
import { App, FrontMcp } from '@frontmcp/sdk';
|
|
141
|
+
|
|
100
142
|
import { WhatsAppChannel } from './apps/messaging/channels/whatsapp.channel';
|
|
101
143
|
|
|
102
144
|
@App({
|
|
@@ -116,7 +158,7 @@ export default class Server {}
|
|
|
116
158
|
## Setup
|
|
117
159
|
|
|
118
160
|
1. Create a WhatsApp Business App at [developers.facebook.com](https://developers.facebook.com)
|
|
119
|
-
2. Set environment variables: `WHATSAPP_TOKEN`, `WHATSAPP_PHONE_ID`, `WHATSAPP_ALLOWED_SENDERS`
|
|
161
|
+
2. Set environment variables: `WHATSAPP_TOKEN`, `WHATSAPP_PHONE_ID`, `WHATSAPP_APP_SECRET`, `WHATSAPP_ALLOWED_SENDERS`
|
|
120
162
|
3. Configure webhook URL to `https://your-server/hooks/whatsapp`
|
|
121
163
|
4. Subscribe to `messages` webhook field
|
|
122
164
|
|
|
@@ -12,6 +12,14 @@ Every channel has a source that determines how events flow into it. FrontMCP sup
|
|
|
12
12
|
Registers an HTTP POST endpoint. External services (GitHub, CI/CD, monitoring) send payloads to this endpoint.
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
|
+
// Webhook payloads have a known shape: { body, headers, method, query? }.
|
|
16
|
+
// Define the slice you need locally rather than importing an internal type.
|
|
17
|
+
interface CIWebhookBody {
|
|
18
|
+
pipeline: string;
|
|
19
|
+
status: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
@Channel({
|
|
16
24
|
name: 'ci-alerts',
|
|
17
25
|
description: 'CI/CD pipeline notifications',
|
|
@@ -19,8 +27,8 @@ Registers an HTTP POST endpoint. External services (GitHub, CI/CD, monitoring) s
|
|
|
19
27
|
})
|
|
20
28
|
class CIAlertChannel extends ChannelContext {
|
|
21
29
|
async onEvent(payload: unknown): Promise<ChannelNotification> {
|
|
22
|
-
const { body } = payload as
|
|
23
|
-
const data = body as
|
|
30
|
+
const { body } = payload as { body: unknown };
|
|
31
|
+
const data = body as CIWebhookBody;
|
|
24
32
|
return {
|
|
25
33
|
content: `CI pipeline "${data.pipeline}" ${data.status}.\nDetails: ${data.url}`,
|
|
26
34
|
meta: { pipeline: data.pipeline, status: data.status },
|
|
@@ -209,6 +217,6 @@ Buffered events are replayed when a new session connects, with `replayed: "true"
|
|
|
209
217
|
| [`job-completion`](../examples/channel-sources/job-completion.md) | Intermediate | Notify Claude Code when background jobs and workflows complete |
|
|
210
218
|
| [`service-connector`](../examples/channel-sources/service-connector.md) | Advanced | Build a persistent service connector that lets Claude send and receive messages through WhatsApp, Telegram, or any messaging API |
|
|
211
219
|
| [`file-watcher`](../examples/channel-sources/file-watcher.md) | Intermediate | Watch files for changes and notify Claude Code in real-time |
|
|
212
|
-
| [`replay-buffer`](../examples/channel-sources/replay-buffer.md) | Advanced | Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline
|
|
220
|
+
| [`replay-buffer`](../examples/channel-sources/replay-buffer.md) | Advanced | Buffer channel events so Claude Code receives them when it connects, even if events occurred while offline. |
|
|
213
221
|
|
|
214
222
|
> See all examples in [`examples/channel-sources/`](../examples/channel-sources/)
|
|
@@ -15,129 +15,63 @@ Two-way channels let external users communicate with Claude Code through messagi
|
|
|
15
15
|
4. **Reply**: Claude calls `channel-reply` tool with response text
|
|
16
16
|
5. **Forward**: `onReply()` sends the reply back to the external platform
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## API Surface
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
import { Channel, ChannelContext, ChannelNotification } from '@frontmcp/sdk';
|
|
22
|
-
|
|
23
|
-
// Store verified senders for security
|
|
24
|
-
const allowedSenders = new Set<string>();
|
|
20
|
+
A two-way channel sets `twoWay: true` and implements two hooks on top of the regular `ChannelContext` interface:
|
|
25
21
|
|
|
22
|
+
```typescript
|
|
26
23
|
@Channel({
|
|
27
24
|
name: 'whatsapp',
|
|
28
|
-
description: 'WhatsApp Business API bridge for Claude Code',
|
|
29
25
|
source: { type: 'webhook', path: '/hooks/whatsapp' },
|
|
30
26
|
twoWay: true,
|
|
31
27
|
meta: { platform: 'whatsapp' },
|
|
32
28
|
})
|
|
33
29
|
export class WhatsAppChannel extends ChannelContext {
|
|
30
|
+
// Inbound: shape the external payload into a notification for Claude
|
|
34
31
|
async onEvent(payload: unknown): Promise<ChannelNotification> {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// WhatsApp Cloud API webhook payload structure
|
|
38
|
-
const entry = (body as any).entry?.[0];
|
|
39
|
-
const change = entry?.changes?.[0];
|
|
40
|
-
const message = change?.value?.messages?.[0];
|
|
41
|
-
const contact = change?.value?.contacts?.[0];
|
|
42
|
-
|
|
43
|
-
if (!message || !contact) {
|
|
44
|
-
return { content: 'WhatsApp: received non-message webhook (status update)' };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const sender = contact.wa_id;
|
|
48
|
-
const senderName = contact.profile?.name ?? sender;
|
|
49
|
-
|
|
50
|
-
// Security: only allow verified senders
|
|
51
|
-
if (!allowedSenders.has(sender)) {
|
|
52
|
-
this.logger.warn(`WhatsApp: rejecting message from unverified sender ${sender}`);
|
|
53
|
-
return {
|
|
54
|
-
content: `WhatsApp: message from unverified sender ${senderName} (${sender}). Add to allowlist to process.`,
|
|
55
|
-
meta: { sender, sender_name: senderName, verified: 'false' },
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const text = message.text?.body ?? '[non-text message]';
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
content: `${senderName}: ${text}`,
|
|
63
|
-
meta: {
|
|
64
|
-
sender,
|
|
65
|
-
sender_name: senderName,
|
|
66
|
-
message_id: message.id,
|
|
67
|
-
chat_id: sender,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
32
|
+
/* see whatsapp-bridge example */
|
|
70
33
|
}
|
|
71
34
|
|
|
35
|
+
// Outbound: forward Claude's reply back to the platform.
|
|
36
|
+
// `meta` carries the keys you returned from onEvent (chat_id, sender, ...).
|
|
72
37
|
async onReply(reply: string, meta?: Record<string, string>): Promise<void> {
|
|
73
|
-
|
|
74
|
-
if (!chatId) {
|
|
75
|
-
this.logger.warn('WhatsApp reply: no chat_id in meta');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Send via WhatsApp Cloud API
|
|
80
|
-
const token = process.env['WHATSAPP_TOKEN'];
|
|
81
|
-
const phoneNumberId = process.env['WHATSAPP_PHONE_ID'];
|
|
82
|
-
|
|
83
|
-
await fetch(`https://graph.facebook.com/v18.0/${phoneNumberId}/messages`, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: {
|
|
86
|
-
Authorization: `Bearer ${token}`,
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
},
|
|
89
|
-
body: JSON.stringify({
|
|
90
|
-
messaging_product: 'whatsapp',
|
|
91
|
-
to: chatId,
|
|
92
|
-
text: { body: reply },
|
|
93
|
-
}),
|
|
94
|
-
});
|
|
38
|
+
/* call platform send-message API */
|
|
95
39
|
}
|
|
96
40
|
}
|
|
97
41
|
```
|
|
98
42
|
|
|
43
|
+
The full WhatsApp implementation, including sender allowlisting and the WhatsApp Cloud API call, lives in [`examples/channel-two-way/whatsapp-bridge.md`](../examples/channel-two-way/whatsapp-bridge.md).
|
|
44
|
+
|
|
99
45
|
## Telegram Bot Bridge
|
|
100
46
|
|
|
47
|
+
Telegram works the same way — different webhook path, different reply API:
|
|
48
|
+
|
|
101
49
|
```typescript
|
|
102
50
|
@Channel({
|
|
103
51
|
name: 'telegram',
|
|
104
|
-
description: 'Telegram bot bridge for Claude Code',
|
|
105
52
|
source: { type: 'webhook', path: '/hooks/telegram' },
|
|
106
53
|
twoWay: true,
|
|
107
54
|
meta: { platform: 'telegram' },
|
|
108
55
|
})
|
|
109
56
|
export class TelegramChannel extends ChannelContext {
|
|
110
57
|
async onEvent(payload: unknown): Promise<ChannelNotification> {
|
|
111
|
-
const { body } = payload as {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
chat: { id: number };
|
|
115
|
-
from: { username?: string; first_name: string; id: number };
|
|
116
|
-
text?: string;
|
|
58
|
+
const { body } = payload as {
|
|
59
|
+
body: {
|
|
60
|
+
message?: { chat: { id: number }; from: { username?: string; first_name: string; id: number }; text?: string };
|
|
117
61
|
};
|
|
118
62
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!msg?.text) {
|
|
122
|
-
return { content: 'Telegram: non-text update received' };
|
|
123
|
-
}
|
|
124
|
-
|
|
63
|
+
const msg = body.message;
|
|
64
|
+
if (!msg?.text) return { content: 'Telegram: non-text update' };
|
|
125
65
|
const sender = msg.from.username ?? msg.from.first_name;
|
|
126
|
-
|
|
127
66
|
return {
|
|
128
67
|
content: `${sender}: ${msg.text}`,
|
|
129
|
-
meta: {
|
|
130
|
-
chat_id: String(msg.chat.id),
|
|
131
|
-
sender: String(msg.from.id),
|
|
132
|
-
sender_name: sender,
|
|
133
|
-
},
|
|
68
|
+
meta: { chat_id: String(msg.chat.id), sender: String(msg.from.id), sender_name: sender },
|
|
134
69
|
};
|
|
135
70
|
}
|
|
136
71
|
|
|
137
72
|
async onReply(reply: string, meta?: Record<string, string>): Promise<void> {
|
|
138
73
|
const chatId = meta?.chat_id;
|
|
139
74
|
if (!chatId) return;
|
|
140
|
-
|
|
141
75
|
const token = process.env['TELEGRAM_BOT_TOKEN'];
|
|
142
76
|
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
143
77
|
method: 'POST',
|
|
@@ -166,16 +100,53 @@ if (isGroupMember(chatId)) {
|
|
|
166
100
|
}
|
|
167
101
|
```
|
|
168
102
|
|
|
169
|
-
### Webhook Verification
|
|
103
|
+
### Webhook Signature Verification
|
|
170
104
|
|
|
171
|
-
|
|
105
|
+
Webhook handlers must verify the request actually came from the platform. Use the HMAC helper from `@frontmcp/utils` so the same code runs in Node and Edge runtimes:
|
|
172
106
|
|
|
173
107
|
```typescript
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
108
|
+
import { hmacSha256 } from '@frontmcp/utils';
|
|
109
|
+
|
|
110
|
+
function verifyWhatsAppSignature(rawBody: string, header: string | undefined, appSecret: string): boolean {
|
|
111
|
+
if (!header?.startsWith('sha256=')) return false;
|
|
112
|
+
const expected = header.slice('sha256='.length);
|
|
113
|
+
|
|
114
|
+
const key = new TextEncoder().encode(appSecret);
|
|
115
|
+
const data = new TextEncoder().encode(rawBody);
|
|
116
|
+
const digest = hmacSha256(key, data);
|
|
117
|
+
|
|
118
|
+
// Constant-time hex compare
|
|
119
|
+
const actual = Array.from(digest, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
120
|
+
if (actual.length !== expected.length) return false;
|
|
121
|
+
let diff = 0;
|
|
122
|
+
for (let i = 0; i < actual.length; i++) diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
123
|
+
return diff === 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// In onEvent:
|
|
127
|
+
//
|
|
128
|
+
// NOTE: WebhookPayload.body is the parsed JSON. For maximum signature fidelity
|
|
129
|
+
// (Meta computes X-Hub-Signature-256 over the exact raw bytes), capture the raw
|
|
130
|
+
// body in a transport-level middleware (e.g. express.json `verify` hook) and
|
|
131
|
+
// pass that string to `verifyWhatsAppSignature` instead of `JSON.stringify(body)`.
|
|
132
|
+
const { body, headers } = payload as { body: unknown; headers: Record<string, string | string[] | undefined> };
|
|
133
|
+
const sig = headers['x-hub-signature-256'];
|
|
134
|
+
if (
|
|
135
|
+
!verifyWhatsAppSignature(JSON.stringify(body), Array.isArray(sig) ? sig[0] : sig, process.env['WHATSAPP_APP_SECRET']!)
|
|
136
|
+
) {
|
|
137
|
+
this.logger.warn('WhatsApp: signature mismatch, dropping');
|
|
138
|
+
return { content: '', meta: { verified: 'false' } };
|
|
139
|
+
}
|
|
177
140
|
```
|
|
178
141
|
|
|
142
|
+
Per-platform header names:
|
|
143
|
+
|
|
144
|
+
| Platform | Header / mechanism |
|
|
145
|
+
| -------- | --------------------------------------------------------------- |
|
|
146
|
+
| WhatsApp | `X-Hub-Signature-256` (HMAC-SHA256 of raw body with app secret) |
|
|
147
|
+
| Telegram | `secret_token` query parameter (compare with the value you set) |
|
|
148
|
+
| Slack | `X-Slack-Signature` + `X-Slack-Request-Timestamp` (HMAC-SHA256) |
|
|
149
|
+
|
|
179
150
|
### Pairing Codes
|
|
180
151
|
|
|
181
152
|
For bootstrapping trust, use pairing codes:
|