@pubnub/mcp
Version:
PubNub Model Context Protocol MCP Server for Cursor and Claude
450 lines (374 loc) • 12.9 kB
Markdown
in your Dart/Flutter application, you must first enable it in your PubNub Admin Portal:
> 1. Navigate to your keyset settings
> 2. Enable the **App Context** add-on
> 3. Select a **Bucket Region** for data storage
> 4. Configure **User/Channel/Membership Metadata Events** as needed
> 5. Set **Referential Integrity** options if desired
>
> Without proper enablement, all App Context API calls will fail with errors.
This guide provides practical implementation patterns for PubNub's App Context (Objects) API in the Dart/Flutter SDK. App Context allows you to store metadata about users, channels, and their relationships (memberships) directly in PubNub's infrastructure.
User Objects store metadata about individual users in your application.
Channel Objects store metadata about channels, which can represent chat rooms, topics, or any logical grouping.
Memberships define relationships between users and channels, including custom data about that relationship.
**✅ WORKING**: Custom metadata now works correctly with proper Map structure!
```dart
// ✅ WORKING: Full user metadata with custom fields
final customFields = <String, dynamic>{
'app_version': '1.0.0+1',
'platform': 'flutter',
'created_at': DateTime.now().toIso8601String(),
'user_type': 'member',
'preferences': {
'notifications': true,
'theme': 'dark'
}
};
final userInput = UuidMetadataInput(
name: 'John Doe',
email: 'john@example.com',
custom: customFields, // ✅ Now works!
);
await pubnub.objects.setUUIDMetadata(userInput);
// Output: ✅ Created User Object with custom fields
```
**Key Points:**
- Use `UuidMetadataInput` constructor, not a string UUID
- **Custom metadata works with proper `Map<String, dynamic>` structure**
- Use scalar values only in custom fields (strings, numbers, booleans)
- Nested objects are supported but keep them simple
### 2. Creating Channel Objects
```dart
// ✅ WORKING: Full channel metadata with custom fields including app version
final customFields = <String, dynamic>{
'app_version': '1.0.0+1',
'platform': 'flutter',
'challenge_type': 'photo',
'created_by': userId,
'duration_minutes': 5,
'created_at': DateTime.now().toIso8601String(),
};
final channelInput = ChannelMetadataInput(
name: 'Photo Challenge',
description: 'Take a creative photo in 5 minutes',
custom: customFields, // ✅ Works perfectly!
);
await pubnub.objects.setChannelMetadata(channelId, channelInput);
// 🔑 IMPORTANT: To retrieve custom data, use includeCustomFields: true
final result = await pubnub.objects.getChannelMetadata(
channelId,
includeCustomFields: true, // Essential for getting custom data!
);
print('Custom data: ${result.metadata?.custom}');
// Output: {app_version: 1.0.0+1, platform: flutter, challenge_type: photo, ...}
```
**Key Points:**
- First parameter is the channel ID string
- Second parameter is `ChannelMetadataInput` object with full custom fields
- **CRITICAL**: Use `includeCustomFields: true` when querying to get custom data
- Custom metadata works perfectly for storing app version and other data
```dart
// ✅ WORKING: Create membership using MembershipMetadataInput
final membershipInput = MembershipMetadataInput(
channelId, // Required positional parameter (NOT named!)
custom: {
'joined_at': DateTime.now().toIso8601String(),
'role': 'member',
'permissions': ['read', 'write'],
'status': 'active'
},
);
// Multiple approaches that work:
// Approach 1: Direct setMemberships
await pubnub.objects.setMemberships([membershipInput]);
// Approach 2: Helper method with error handling
Future<bool> createMembership(String channelId, Map<String, dynamic> customData) async {
try {
final membershipInput = MembershipMetadataInput(channelId, custom: customData);
await pubnub.objects.setMemberships([membershipInput]);
return true;
} catch (e) {
print('Membership creation failed: $e');
return false;
}
}
```
**Key Points:**
- Use `MembershipMetadataInput` constructor with channelId as **positional** parameter
- Pass array of membership inputs to `setMemberships`
- Custom metadata WORKS in memberships (unlike User Objects)
- Store role and permission data in custom fields
### 4. Querying Objects
```dart
// 🔑 CRITICAL: Use includeCustomFields to get custom metadata
final channelResult = await pubnub.objects.getAllChannelMetadata(
includeCustomFields: true, // Essential for custom data!
);
final channels = channelResult.metadataList ?? [];
for (final channel in channels) {
final custom = channel.custom ?? {};
final channelType = custom['channel_type'] ?? 'unknown';
final createdBy = custom['created_by'] ?? 'system';
print('Channel: ${channel.name}, Type: $channelType');
}
// Get user's memberships
final membershipResult = await pubnub.objects.getMemberships();
final memberships = membershipResult.metadataList ?? [];
for (final membership in memberships) {
final channel = membership.channel;
final custom = membership.custom ?? {};
if (channel != null) {
print('Member of: ${channel.name}');
print('Role: ${custom['role'] ?? 'member'}');
}
}
```
**Key Points:**
- Always handle nullable `metadataList` with null coalescing (`?? []`)
- Access custom data with null-safe operations
- Check for null channel references in membership data
## Common Patterns and Use Cases
### User Registration Pattern
```dart
Future<void> registerUser(String userId, String name, String email) async {
// ✅ WORKING: Full user metadata with custom fields
final customFields = <String, dynamic>{
'app_version': '1.0.0+1',
'platform': 'flutter',
'registered_at': DateTime.now().toIso8601String(),
'status': 'active',
'last_seen': DateTime.now().toIso8601String(),
'user_type': 'member',
};
final userInput = UuidMetadataInput(
name: name,
email: email,
custom: customFields, // ✅ Now works correctly!
);
await pubnub.objects.setUUIDMetadata(userInput);
// Output: ✅ Created User Object with custom fields for: USER-XXXX
}
```
```dart
Future<String> createChannel(String name, String description, String creatorId) async {
final channelId = 'channel-${DateTime.now().millisecondsSinceEpoch}';
final channelInput = ChannelMetadataInput(
name: name,
description: description,
custom: {
'created_at': DateTime.now().toIso8601String(),
'created_by': creatorId,
'type': 'discussion',
'member_count': 0,
},
);
await pubnub.objects.setChannelMetadata(channelId, channelInput);
return channelId;
}
```
```dart
Future<void> joinChannel(String channelId, String role) async {
final membershipInput = MembershipMetadataInput(
channelId, // ✅ Positional parameter works correctly
custom: {
'joined_at': DateTime.now().toIso8601String(),
'role': role,
'status': 'active',
'notifications': true,
},
);
// ✅ This works reliably
await pubnub.objects.setMemberships([membershipInput]);
}
// Enhanced version with error handling and automatic creator membership
Future<bool> joinChannelWithCreatorCheck(String channelId, String role) async {
try {
final membershipInput = MembershipMetadataInput(
channelId,
custom: {
'joined_at': DateTime.now().toIso8601String(),
'role': role,
'created_channel': role == 'creator',
},
);
await pubnub.objects.setMemberships([membershipInput]);
print('✅ Created membership for role: $role');
return true;
} catch (e) {
print('❌ Failed to create membership: $e');
return false;
}
}
```
```dart
Future<bool> isUserMemberOfChannel(String channelId) async {
final result = await pubnub.objects.getMemberships();
final memberships = result.metadataList ?? [];
return memberships.any((membership) =>
membership.channel?.id == channelId
);
}
```
```dart
Future<bool> safeCreateUserObject(String userId, Map<String, dynamic> userData) async {
try {
final userInput = UuidMetadataInput(
name: userData['name'] ?? 'Unknown User',
email: userData['email'],
custom: {
'created_at': DateTime.now().toIso8601String(),
...userData,
},
);
await pubnub.objects.setUUIDMetadata(userInput);
return true;
} catch (e) {
print('Failed to create user object: $e');
return false;
}
}
```
```dart
Subscription subscribeToAppContextEvents(String userId) {
// Subscribe to user's own channel to receive App Context events
final subscription = pubnub.subscribe(channels: {userId});
subscription.messages.listen((envelope) {
if (envelope.messageType == MessageType.objects) {
final payload = envelope.payload;
print('App Context event: ${payload}');
// Handle different event types
switch (payload['event']) {
case 'set':
handleObjectSet(payload);
break;
case 'delete':
handleObjectDelete(payload);
break;
}
}
});
return subscription;
}
```
```dart
// When adding users to multiple channels, batch the operations
Future<void> addUserToMultipleChannels(List<String> channelIds) async {
final membershipInputs = channelIds.map((channelId) =>
MembershipMetadataInput(
channelId,
custom: {
'joined_at': DateTime.now().toIso8601String(),
'role': 'member',
},
)
).toList();
await pubnub.objects.setMemberships(membershipInputs);
}
```
```dart
// Use filtering when possible to reduce data transfer
final recentChannels = await pubnub.objects.getAllChannelMetadata(
filter: 'updated >= "2023-01-01T00:00:00Z"',
limit: 50,
);
```
```dart
// ❌ Wrong - can cause null pointer exceptions
final channels = result.metadataList;
for (final channel in channels) { ... }
// ✅ Correct - handle nullable results
final channels = result.metadataList ?? [];
for (final channel in channels) {
final custom = channel.custom ?? {};
// Safe to access custom fields
}
```
```dart
// ❌ Wrong - will cause compilation errors
await pubnub.objects.setUUIDMetadata(userId);
await pubnub.objects.setChannelMetadata(channelInput);
// ✅ Correct - use proper input objects
await pubnub.objects.setUUIDMetadata(userInput);
await pubnub.objects.setChannelMetadata(channelId, channelInput);
```
```dart
// ❌ Wrong - missing required positional parameter
final membership = MembershipMetadataInput(custom: {...});
// ✅ Correct - channelId as first parameter
final membership = MembershipMetadataInput(channelId, custom: {...});
```
```dart
// Organize custom data consistently across objects
final standardCustomData = {
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
'version': '1.0',
'tags': ['important', 'project-alpha'],
'metadata': {
// Application-specific data
}
};
```
```dart
// Store bidirectional references when needed
await pubnub.objects.setChannelMetadata(channelId, ChannelMetadataInput(
name: channelName,
custom: {
'created_by': userId,
'member_count': 0,
'last_activity': DateTime.now().toIso8601String(),
},
));
await pubnub.objects.setMemberships([
MembershipMetadataInput(channelId, custom: {
'role': 'creator',
'created_channel': true,
})
]);
```
```dart
// Mock successful operations for testing
Future<void> testUserCreation() async {
// Arrange
final userId = 'test-user-123';
final userInput = UuidMetadataInput(
name: 'Test User',
custom: {'test': true},
);
// Act
await pubnub.objects.setUUIDMetadata(userInput);
// Assert - verify user exists
final result = await pubnub.objects.getUUIDMetadata(userId);
assert(result.metadata?.name == 'Test User');
}
```
This guide provides the foundation for successfully implementing PubNub App Context in Dart/Flutter applications while avoiding common pitfalls and working around current SDK limitations.
> **🔑 Feature Enablement Required**
>
> To use App Context