Skip to content

Commit d9ef8c2

Browse files
Add mcpclient for evaluation sub package to call the mobile-web mcp tool (#20)
* feat: setup basic evaluation frame work * feat: setup mobileWebMcpClient for evaluation * move README.md up to root of evaluation sub package * fix a test failure
1 parent 63ac2ff commit d9ef8c2

File tree

6 files changed

+134
-7
lines changed

6 files changed

+134
-7
lines changed

.codecov.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,3 @@ parsers:
6767
loop: yes
6868
method: no
6969
macro: no
70-
71-
macro: no

.github/workflows/run-tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
node-version: ${{ matrix.node }}
1717
cache: 'npm'
1818
- run: npm install
19+
- run: npm run build:all
1920
- run: npm run test:coverage
2021
# Only run Xvfb cleanup on Ubuntu (Unix systems)
2122
- if: matrix.os == 'ubuntu-latest'
@@ -30,6 +31,13 @@ jobs:
3031
flags: mobile-web
3132
name: mobile-web-coverage
3233

34+
- name: Upload evaluation coverage
35+
uses: codecov/codecov-action@v3
36+
with:
37+
token: ${{ secrets.CODECOV_TOKEN }}
38+
files: packages/evaluation/coverage/*.json
39+
flags: evaluation
40+
name: evaluation-coverage
3341
# Add more packages as you create them
3442
# - name: Upload backend-api coverage
3543
# uses: codecov/codecov-action@v3

package-lock.json

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@
2929
"url": "https://github.com/forcedotcom/mobile-mcp-tools/issues"
3030
},
3131
"homepage": "https://github.com/forcedotcom/mobile-mcp-tools#readme",
32+
"dependencies": {
33+
"@modelcontextprotocol/sdk": "^1.12.3"
34+
},
3235
"devDependencies": {
3336
"@eslint/js": "^9.30.0",
3437
"@nx/workspace": "^21.2.1",
38+
"@types/node": "^24.0.10",
39+
"@vitest/coverage-v8": "^3.2.3",
3540
"@typescript-eslint/eslint-plugin": "^8.35.1",
3641
"eslint": "^9.30.0",
3742
"eslint-config-prettier": "^10.1.5",
@@ -45,7 +50,7 @@
4550
"typescript": "^5.8.3",
4651
"typescript-eslint": "^8.35.1",
4752
"vite": "^6.3.5",
48-
"zod": "^3.25.0"
53+
"zod": "^3.25.67"
4954
},
5055
"nx": {
5156
"extends": "@nx/workspace/presets/npm.json"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
**/
7+
8+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
9+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
10+
11+
// Path to the MCP server entry point
12+
const SERVER_COMMAND = 'ts-node';
13+
const SERVER_ARGS = [require.resolve('../../../mobile-web/dist/index.js')];
14+
15+
export class MobileWebMcpClient {
16+
private client: Client;
17+
private transport: StdioClientTransport;
18+
19+
constructor() {
20+
this.transport = new StdioClientTransport({
21+
command: SERVER_COMMAND,
22+
args: SERVER_ARGS,
23+
});
24+
this.client = new Client({
25+
name: 'MobileWebMcpClient',
26+
version: '1.0.0',
27+
});
28+
}
29+
30+
async connect() {
31+
await this.client.connect(this.transport);
32+
}
33+
34+
async disconnect() {
35+
await this.client.close();
36+
}
37+
38+
async listTools() {
39+
return this.client.listTools();
40+
}
41+
42+
async callTool(toolName: string, params: Record<string, any>) {
43+
return this.client.callTool({ name: toolName, arguments: params });
44+
}
45+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { MobileWebMcpClient } from '../../src/mcpclient/mobileWebMcpClient.js';
3+
import { spawn } from 'child_process';
4+
import { execSync } from 'child_process';
5+
import path from 'path';
6+
7+
// Integration tests against the real MCP server
8+
describe('MobileWebMcpClient (integration)', () => {
9+
let client: MobileWebMcpClient;
10+
let serverProcess: import('child_process').ChildProcess;
11+
12+
beforeAll(async () => {
13+
// Start the MCP server
14+
serverProcess = spawn('npm', ['run', 'mobile-web:server:start'], {
15+
cwd: path.resolve(__dirname, '../../../..'),
16+
stdio: 'inherit',
17+
shell: true,
18+
});
19+
20+
// Wait for the server to be ready (simple delay, adjust as needed)
21+
await new Promise(resolve => setTimeout(resolve, 5000));
22+
23+
client = new MobileWebMcpClient();
24+
await client.connect();
25+
});
26+
27+
afterAll(async () => {
28+
if (client) {
29+
await client.disconnect();
30+
}
31+
if (serverProcess) {
32+
serverProcess.kill();
33+
}
34+
});
35+
36+
const toolNameToGroundingTitleMapping: Record<string, string> = {
37+
'sfmobile-web-app-review': 'App Review Service Grounding Context',
38+
'sfmobile-web-ar-space-capture': 'AR Space Capture Service Grounding Context',
39+
'sfmobile-web-barcode-scanner': 'Barcode Scanner Service Grounding Context',
40+
'sfmobile-web-biometrics': 'Biometrics Service Grounding Context',
41+
'sfmobile-web-calendar': 'Calendar Service Grounding Context',
42+
'sfmobile-web-contacts': 'Contacts Service Grounding Context',
43+
'sfmobile-web-document-scanner': 'Document Scanner Service Grounding Context',
44+
'sfmobile-web-geofencing': 'Geofencing Service Grounding Context',
45+
'sfmobile-web-location': 'Location Service Grounding Context',
46+
'sfmobile-web-nfc': 'NFC Service Grounding Context',
47+
'sfmobile-web-payments': 'Payments Service Grounding Context',
48+
};
49+
50+
it('should list available tools', async () => {
51+
const result = await client.listTools();
52+
const toolNames = result.tools.map(tool => tool.name);
53+
expect(toolNames).toEqual(expect.arrayContaining(Object.keys(toolNameToGroundingTitleMapping)));
54+
});
55+
56+
// Test each tool with the correct grounding context
57+
it.each(Object.keys(toolNameToGroundingTitleMapping))(
58+
'should call a tool %s and return correct grounding context',
59+
async toolName => {
60+
const groundTitle = toolNameToGroundingTitleMapping[toolName];
61+
const result = await client.callTool(toolName, {});
62+
const groundingContext = result.content?.[0]?.text;
63+
expect(groundingContext).toEqual(expect.stringContaining(groundTitle));
64+
}
65+
);
66+
});

0 commit comments

Comments
 (0)