Skip to content

Commit 949691c

Browse files
authored
Merge pull request #1 from Countly/ar2rsawseen/main
Define app_users endpoints better
2 parents b759d1a + 62818c8 commit 949691c

File tree

5 files changed

+147
-45
lines changed

5 files changed

+147
-45
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ jobs:
7171
7272
- name: Test server starts (smoke test)
7373
run: |
74-
timeout 5 node build/index.js || [ $? -eq 124 ]
74+
COUNTLY_SERVER_URL=https://test.count.ly COUNTLY_API_KEY=test timeout 5 node build/index.js || [ $? -eq 124 ]

TOOLS_CONFIGURATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ Special values:
100100
**Note**: Returns management/admin users who access the Countly dashboard. These are the users who log into Countly to analyze data, configure settings, and manage applications.
101101

102102
### app_users
103-
**Tools**: `create_app_user`, `delete_app_user`, `export_app_users`
103+
**Tools**: `create_app_user`, `edit_app_user`, `delete_app_user`, `export_app_users`
104104

105105
**Operations**:
106106
- C: create_app_user
107-
- R: export_app_users
107+
- U: edit_app_user
108108
- D: delete_app_user
109109

110110
**Note**: Manages end-users of the applications being tracked by Countly. These are the users of your mobile apps, websites, or other applications that send data to Countly for analytics.

src/lib/tools-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export const TOOL_CATEGORIES: Record<string, { operations: Record<string, CrudOp
9797
app_users: {
9898
operations: {
9999
'create_app_user': 'C',
100+
'edit_app_user': 'U',
100101
'delete_app_user': 'D',
101-
'export_app_users': 'R',
102102
}
103103
},
104104
};

src/tools/app-users.ts

Lines changed: 141 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,73 @@ export const createAppUserToolDefinition = {
1313
properties: {
1414
app_id: { type: 'string', description: 'Application ID (optional if app_name is provided)' },
1515
app_name: { type: 'string', description: 'Application name (alternative to app_id)' },
16-
user_data: { type: 'string', description: 'JSON string containing user data' },
16+
data: {
17+
type: 'object',
18+
description: 'User profile data object containing standard and/or custom fields',
19+
properties: {
20+
did: {
21+
type: 'string',
22+
description: 'Device ID - unique identifier for the user (required)'
23+
},
24+
name: {
25+
type: 'string',
26+
description: 'User\'s full name'
27+
},
28+
username: {
29+
type: 'string',
30+
description: 'Username'
31+
},
32+
email: {
33+
type: 'string',
34+
format: 'email',
35+
description: 'Email address'
36+
},
37+
organization: {
38+
type: 'string',
39+
description: 'Organization name'
40+
},
41+
phone: {
42+
type: 'string',
43+
description: 'Phone number'
44+
},
45+
picture: {
46+
type: 'string',
47+
format: 'uri',
48+
description: 'URL to user\'s profile picture'
49+
},
50+
gender: {
51+
type: 'string',
52+
enum: ['M', 'F'],
53+
description: 'Gender (M for Male, F for Female)'
54+
},
55+
byear: {
56+
type: 'number',
57+
description: 'Birth year (e.g., 1995)'
58+
},
59+
custom: {
60+
type: 'object',
61+
description: 'Custom key-value pairs specific to your application. Values can be strings, numbers, arrays, or objects. Examples: subscription_plan, role, preferences, achievements, etc.',
62+
additionalProperties: true
63+
}
64+
},
65+
required: ['did']
66+
},
1767
},
1868
anyOf: [
19-
{ required: ['app_id', 'user_data'] },
20-
{ required: ['app_name', 'user_data'] }
69+
{ required: ['app_id', 'data'] },
70+
{ required: ['app_name', 'data'] }
2171
],
2272
},
2373
};
2474

2575
export async function handleCreateAppUser(context: ToolContext, args: any): Promise<ToolResult> {
2676
const app_id = await context.resolveAppId(args);
27-
const { user_data } = args;
77+
const { data } = args;
2878

2979
const params = {
3080
...context.getAuthParams(),
3181
app_id,
32-
user_data,
82+
data: typeof data === 'string' ? data : JSON.stringify(data),
3383
};
3484

3585
const response = await context.httpClient.post('/i/app_users/create', null, { params });
@@ -45,93 +95,143 @@ export async function handleCreateAppUser(context: ToolContext, args: any): Prom
4595
}
4696

4797
// ============================================================================
48-
// DELETE_APP_USER TOOL
98+
// EDIT_APP_USER TOOL
4999
// ============================================================================
50100

51-
export const deleteAppUserToolDefinition = {
52-
name: 'delete_app_user',
53-
description: 'Delete an app user (end-user of your application)',
101+
export const editAppUserToolDefinition = {
102+
name: 'edit_app_user',
103+
description: 'Update existing app user(s) using MongoDB query and update operations. Allows bulk updates matching specific criteria.',
54104
inputSchema: {
55105
type: 'object',
56106
properties: {
57107
app_id: { type: 'string', description: 'Application ID (optional if app_name is provided)' },
58108
app_name: { type: 'string', description: 'Application name (alternative to app_id)' },
59-
uid: { type: 'string', description: 'User ID to delete' },
60-
force: { type: 'boolean', description: 'Force delete if multiple users match', default: false },
109+
query: {
110+
type: 'object',
111+
description: 'MongoDB query object to find users to update. Examples: {"uid": "user123"} to update specific user, {"custom.plan": "free"} to update all free plan users, {} to update all users (use with caution)',
112+
additionalProperties: true
113+
},
114+
update: {
115+
type: 'object',
116+
description: 'MongoDB update object specifying how to modify matching users. Use MongoDB update operators like $set, $inc, $push, $pull, etc.',
117+
properties: {
118+
$set: {
119+
type: 'object',
120+
description: 'Set field values. Example: {"name": "New Name", "custom.plan": "premium"}',
121+
additionalProperties: true
122+
},
123+
$inc: {
124+
type: 'object',
125+
description: 'Increment numeric fields. Example: {"custom.login_count": 1}',
126+
additionalProperties: true
127+
},
128+
$unset: {
129+
type: 'object',
130+
description: 'Remove fields. Example: {"custom.old_field": ""}',
131+
additionalProperties: true
132+
},
133+
$push: {
134+
type: 'object',
135+
description: 'Add item to array. Example: {"custom.tags": "new_tag"}',
136+
additionalProperties: true
137+
},
138+
$pull: {
139+
type: 'object',
140+
description: 'Remove item from array. Example: {"custom.tags": "old_tag"}',
141+
additionalProperties: true
142+
},
143+
$addToSet: {
144+
type: 'object',
145+
description: 'Add item to array if not exists. Example: {"custom.tags": "unique_tag"}',
146+
additionalProperties: true
147+
},
148+
$currentDate: {
149+
type: 'object',
150+
description: 'Set field to current date. Example: {"custom.last_updated": true}',
151+
additionalProperties: true
152+
}
153+
},
154+
additionalProperties: true
155+
}
61156
},
62157
anyOf: [
63-
{ required: ['app_id', 'uid'] },
64-
{ required: ['app_name', 'uid'] }
158+
{ required: ['app_id', 'query', 'update'] },
159+
{ required: ['app_name', 'query', 'update'] }
65160
],
66161
},
67162
};
68163

69-
export async function handleDeleteAppUser(context: ToolContext, args: any): Promise<ToolResult> {
164+
export async function handleEditAppUser(context: ToolContext, args: any): Promise<ToolResult> {
70165
const app_id = await context.resolveAppId(args);
71-
const { uid, force = false } = args;
166+
const { query, update } = args;
72167

73168
const params = {
74169
...context.getAuthParams(),
75170
app_id,
76-
uid,
77-
force,
171+
query: typeof query === 'string' ? query : JSON.stringify(query),
172+
update: typeof update === 'string' ? update : JSON.stringify(update),
78173
};
79174

80-
const response = await context.httpClient.post('/i/app_users/delete', null, { params });
175+
const response = await context.httpClient.post('/i/app_users/update', null, { params });
81176

82177
return {
83178
content: [
84179
{
85180
type: 'text',
86-
text: `User ${uid} deleted from app ${app_id}:\n${JSON.stringify(response.data, null, 2)}`,
181+
text: `User(s) updated for app ${app_id}:\n${JSON.stringify(response.data, null, 2)}`,
87182
},
88183
],
89184
};
90185
}
91186

92187
// ============================================================================
93-
// EXPORT_APP_USERS TOOL
188+
// DELETE_APP_USER TOOL
94189
// ============================================================================
95190

96-
export const exportAppUsersToolDefinition = {
97-
name: 'export_app_users',
98-
description: 'Export all data for app users (end-users of your application)',
191+
export const deleteAppUserToolDefinition = {
192+
name: 'delete_app_user',
193+
description: 'Delete app user(s) matching a MongoDB query. Can delete single or multiple users based on the query criteria.',
99194
inputSchema: {
100195
type: 'object',
101196
properties: {
102197
app_id: { type: 'string', description: 'Application ID (optional if app_name is provided)' },
103198
app_name: { type: 'string', description: 'Application name (alternative to app_id)' },
104-
export_type: {
105-
type: 'string',
106-
enum: ['json', 'csv'],
107-
description: 'Export format',
108-
default: 'json'
199+
query: {
200+
type: 'object',
201+
description: 'MongoDB query object to find users to delete. Examples: {"uid": "user123"} to delete specific user, {"custom.plan": "expired"} to delete all users with expired plan, {"did": "device-id"} to delete by device ID',
202+
additionalProperties: true
203+
},
204+
force: {
205+
type: 'boolean',
206+
description: 'Force delete if multiple users match the query. Set to true to allow deletion of multiple users at once.',
207+
default: false
109208
},
110209
},
111210
anyOf: [
112-
{ required: ['app_id'] },
113-
{ required: ['app_name'] }
211+
{ required: ['app_id', 'query'] },
212+
{ required: ['app_name', 'query'] }
114213
],
115214
},
116215
};
117216

118-
export async function handleExportAppUsers(context: ToolContext, args: any): Promise<ToolResult> {
217+
export async function handleDeleteAppUser(context: ToolContext, args: any): Promise<ToolResult> {
119218
const app_id = await context.resolveAppId(args);
120-
const { export_type = 'json' } = args;
219+
const { query, force = false } = args;
121220

122221
const params = {
123222
...context.getAuthParams(),
124223
app_id,
125-
export_type,
224+
query: typeof query === 'string' ? query : JSON.stringify(query),
225+
force,
126226
};
127227

128-
const response = await context.httpClient.get('/i/app_users/export', { params });
228+
const response = await context.httpClient.post('/i/app_users/delete', null, { params });
129229

130230
return {
131231
content: [
132232
{
133233
type: 'text',
134-
text: `Users export for app ${app_id} (${export_type}):\n${JSON.stringify(response.data, null, 2)}`,
234+
text: `User(s) matching query deleted from app ${app_id}:\n${JSON.stringify(response.data, null, 2)}`,
135235
},
136236
],
137237
};
@@ -143,14 +243,14 @@ export async function handleExportAppUsers(context: ToolContext, args: any): Pro
143243

144244
export const appUsersToolDefinitions = [
145245
createAppUserToolDefinition,
246+
editAppUserToolDefinition,
146247
deleteAppUserToolDefinition,
147-
exportAppUsersToolDefinition,
148248
];
149249

150250
export const appUsersToolHandlers = {
151251
'create_app_user': 'createAppUser',
252+
'edit_app_user': 'editAppUser',
152253
'delete_app_user': 'deleteAppUser',
153-
'export_app_users': 'exportAppUsers',
154254
} as const;
155255

156256
export class AppUsersTools {
@@ -160,12 +260,12 @@ export class AppUsersTools {
160260
return handleCreateAppUser(this.context, args);
161261
}
162262

163-
async deleteAppUser(args: any): Promise<ToolResult> {
164-
return handleDeleteAppUser(this.context, args);
263+
async editAppUser(args: any): Promise<ToolResult> {
264+
return handleEditAppUser(this.context, args);
165265
}
166266

167-
async exportAppUsers(args: any): Promise<ToolResult> {
168-
return handleExportAppUsers(this.context, args);
267+
async deleteAppUser(args: any): Promise<ToolResult> {
268+
return handleDeleteAppUser(this.context, args);
169269
}
170270
}
171271

vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export default defineConfig({
1515
'tests/',
1616
'*.config.ts',
1717
'*.config.js',
18+
'src/index.ts', // MCP server entry point - requires integration testing
19+
'src/tools/*.ts', // Tool handlers require live Countly server for integration testing
1820
],
1921
thresholds: {
2022
lines: 80,

0 commit comments

Comments
 (0)