@coldiq/mcp 0.3.18 → 0.3.20
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/dist/tools/find-people.d.ts +1 -0
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +1 -0
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/get-session-usage.d.ts.map +1 -1
- package/dist/tools/get-session-usage.js +38 -7
- package/dist/tools/get-session-usage.js.map +1 -1
- package/package.json +1 -1
- package/src/tools/find-people.ts +1 -0
- package/src/tools/get-session-usage.ts +40 -7
- package/tests/find-people-schema.test.ts +18 -0
- package/tests/session-usage.test.ts +32 -0
|
@@ -9,6 +9,7 @@ export declare const findPeopleSchema: {
|
|
|
9
9
|
locations: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
10
10
|
keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
11
11
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
max_per_company: z.ZodOptional<z.ZodNumber>;
|
|
12
13
|
fields: z.ZodDefault<z.ZodEnum<["compact", "verbose"]>>;
|
|
13
14
|
reveal: z.ZodOptional<z.ZodBoolean>;
|
|
14
15
|
use_providers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-people.d.ts","sourceRoot":"","sources":["../../src/tools/find-people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,cAAc,gBAAgB,CAAA;AAE3C,eAAO,MAAM,qBAAqB,QAWiJ,CAAA;AAEnL,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"find-people.d.ts","sourceRoot":"","sources":["../../src/tools/find-people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,cAAc,gBAAgB,CAAA;AAE3C,eAAO,MAAM,qBAAqB,QAWiJ,CAAA;AAEnL,eAAO,MAAM,gBAAgB;;;;;;;;;;;;CAY5B,CAAA;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;GAErE"}
|
|
@@ -21,6 +21,7 @@ export const findPeopleSchema = {
|
|
|
21
21
|
locations: z.array(z.string()).optional().describe('Person locations as either ISO-2 country codes ("BR") or English country names ("Brazil") — both are accepted and normalized internally. LeadsFactory uses these to scope contacts to the country; other providers fall back to free-text matching.'),
|
|
22
22
|
keywords: z.array(z.string()).optional().describe('Free-text keyword search terms (e.g. ["growth", "AI"])'),
|
|
23
23
|
limit: z.number().min(1).max(500).default(25).describe('Max results across all companies combined (default: 25, max: 500). For multiple companies multiply: e.g. 10 companies × 5 each = 50. Note: when the matched provider is Apollo, results are capped at 100 per call (Apollo upstream limit).'),
|
|
24
|
+
max_per_company: z.number().min(1).optional().describe('Cap the number of contacts returned per company (e.g. 3 for a buying committee). Applied across all matched companies, before the global limit.'),
|
|
24
25
|
fields: z.enum(['compact', 'verbose']).default('compact').describe('Response shape. "compact" (default) returns a small, predictable per-person record: name, title, linkedin_url, company {name, domain, linkedin_url, headcount}, plus seniority/email/location when the upstream provider includes them. "verbose" returns the raw provider passthrough (~30KB+ per record from FullEnrich: employment history, every company office, descriptions, skills, languages) — only use when you specifically need those fields.'),
|
|
25
26
|
reveal: z.boolean().optional().describe('Apollo-only: when true, follow the obfuscated Apollo people-search results with /apollo/people/bulk-match to reveal full names, emails, and LinkedIn URLs. Costs 1 extra credit per revealed person on top of the search credit. Has no effect for other providers (their data is already unobfuscated). Default false to protect credits.'),
|
|
26
27
|
use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_people').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-people.js","sourceRoot":"","sources":["../../src/tools/find-people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAEzE,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAE3C,MAAM,CAAC,MAAM,qBAAqB,GAChC,kDAAkD;IAClD,8MAA8M;IAC9M,qEAAqE;IACrE,uGAAuG;IACvG,yHAAyH;IACzH,uIAAuI;IACvI,iQAAiQ;IACjQ,6FAA6F;IAC7F,uHAAuH;IACvH,oMAAoM;IACpM,iLAAiL,CAAA;AAEnL,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gHAAgH,CAAC;IAChL,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kHAAkH,CAAC;IAC5K,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;IACtG,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uDAAuD,CAAC;IAC7G,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qPAAqP,CAAC;IACzS,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;IAC3G,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,6OAA6O,CAAC;IACrS,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,2bAA2b,CAAC;IAC/f,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4UAA4U,CAAC;IACrX,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CACjW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAA8B;IACpE,OAAO,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAC1C,CAAC"}
|
|
1
|
+
{"version":3,"file":"find-people.js","sourceRoot":"","sources":["../../src/tools/find-people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAEzE,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAE3C,MAAM,CAAC,MAAM,qBAAqB,GAChC,kDAAkD;IAClD,8MAA8M;IAC9M,qEAAqE;IACrE,uGAAuG;IACvG,yHAAyH;IACzH,uIAAuI;IACvI,iQAAiQ;IACjQ,6FAA6F;IAC7F,uHAAuH;IACvH,oMAAoM;IACpM,iLAAiL,CAAA;AAEnL,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gHAAgH,CAAC;IAChL,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kHAAkH,CAAC;IAC5K,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;IACtG,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uDAAuD,CAAC;IAC7G,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qPAAqP,CAAC;IACzS,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;IAC3G,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,6OAA6O,CAAC;IACrS,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iJAAiJ,CAAC;IACzM,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,2bAA2b,CAAC;IAC/f,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4UAA4U,CAAC;IACrX,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CACjW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAA8B;IACpE,OAAO,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAC1C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-session-usage.d.ts","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-session-usage.d.ts","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,sBAAsB,CAAA;AAEtD,eAAO,MAAM,0BAA0B,QAOY,CAAA;AAEnD,eAAO,MAAM,qBAAqB,IAAK,CAAA;AAMvC,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;GA8B3E"}
|
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
import { getSessionUsage } from '../session-usage.js';
|
|
2
|
+
import { callApi } from '../client.js';
|
|
2
3
|
export const getSessionUsageName = 'get_session_usage';
|
|
3
|
-
export const getSessionUsageDescription = 'Report the ColdIQ credits spent so far in
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
4
|
+
export const getSessionUsageDescription = 'Report the ColdIQ credits spent so far in this MCP process: total credits charged, billed-call count, ' +
|
|
5
|
+
'and a per-endpoint breakdown (highest spend first), plus the live authoritative balance for reconciliation. ' +
|
|
6
|
+
'The session tally is BEST-EFFORT — it sums what each call reported at request time and therefore: (a) cannot ' +
|
|
7
|
+
'see out-of-band async settlement (webhook refunds/charges that land after the call returns), (b) excludes ' +
|
|
8
|
+
'transport failures that never reached the server, and (c) persists for the life of the MCP process, so a ' +
|
|
9
|
+
'reused process can include earlier conversations. Treat session_spend as indicative; use authoritative_balance ' +
|
|
10
|
+
'(or get_credit_balance) as the source of truth.';
|
|
9
11
|
export const getSessionUsageSchema = {};
|
|
12
|
+
const SESSION_USAGE_NOTE = 'session_spend is a best-effort, header-derived tally for this MCP process; it excludes out-of-band async ' +
|
|
13
|
+
'settlement and resets only when the process restarts. Reconcile against authoritative_balance (live).';
|
|
10
14
|
export async function getSessionUsageHandler(_input) {
|
|
11
15
|
const usage = getSessionUsage();
|
|
12
|
-
|
|
16
|
+
// Reconcile against the authoritative balance. /me/credits is not billed, so this
|
|
17
|
+
// call never pollutes the tally. Best-effort: a failure just omits the field.
|
|
18
|
+
let authoritativeBalance;
|
|
19
|
+
try {
|
|
20
|
+
const res = await callApi('GET', '/me/credits');
|
|
21
|
+
if (res.ok && res.data && typeof res.data === 'object') {
|
|
22
|
+
const bal = res.data.data?.balance;
|
|
23
|
+
if (typeof bal === 'number')
|
|
24
|
+
authoritativeBalance = bal;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore — reconciliation is best-effort
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: JSON.stringify({
|
|
35
|
+
data: {
|
|
36
|
+
...usage,
|
|
37
|
+
...(authoritativeBalance !== undefined ? { authoritative_balance: authoritativeBalance } : {}),
|
|
38
|
+
note: SESSION_USAGE_NOTE,
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
13
44
|
}
|
|
14
45
|
//# sourceMappingURL=get-session-usage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-session-usage.js","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"get-session-usage.js","sourceRoot":"","sources":["../../src/tools/get-session-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAEtD,MAAM,CAAC,MAAM,0BAA0B,GACrC,wGAAwG;IACxG,8GAA8G;IAC9G,+GAA+G;IAC/G,4GAA4G;IAC5G,2GAA2G;IAC3G,iHAAiH;IACjH,iDAAiD,CAAA;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAA;AAEvC,MAAM,kBAAkB,GACtB,2GAA2G;IAC3G,uGAAuG,CAAA;AAEzG,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAA+B;IAC1E,MAAM,KAAK,GAAG,eAAe,EAAE,CAAA;IAE/B,kFAAkF;IAClF,8EAA8E;IAC9E,IAAI,oBAAwC,CAAA;IAC5C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,aAAa,CAAC,CAAA;QAC/C,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvD,MAAM,GAAG,GAAI,GAAG,CAAC,IAAyC,CAAC,IAAI,EAAE,OAAO,CAAA;YACxE,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,oBAAoB,GAAG,GAAG,CAAA;QACzD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,yCAAyC;IAC3C,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,IAAI,EAAE;wBACJ,GAAG,KAAK;wBACR,GAAG,CAAC,oBAAoB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC9F,IAAI,EAAE,kBAAkB;qBACzB;iBACF,CAAC;aACH;SACF;KACF,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
package/src/tools/find-people.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const findPeopleSchema = {
|
|
|
25
25
|
locations: z.array(z.string()).optional().describe('Person locations as either ISO-2 country codes ("BR") or English country names ("Brazil") — both are accepted and normalized internally. LeadsFactory uses these to scope contacts to the country; other providers fall back to free-text matching.'),
|
|
26
26
|
keywords: z.array(z.string()).optional().describe('Free-text keyword search terms (e.g. ["growth", "AI"])'),
|
|
27
27
|
limit: z.number().min(1).max(500).default(25).describe('Max results across all companies combined (default: 25, max: 500). For multiple companies multiply: e.g. 10 companies × 5 each = 50. Note: when the matched provider is Apollo, results are capped at 100 per call (Apollo upstream limit).'),
|
|
28
|
+
max_per_company: z.number().min(1).optional().describe('Cap the number of contacts returned per company (e.g. 3 for a buying committee). Applied across all matched companies, before the global limit.'),
|
|
28
29
|
fields: z.enum(['compact', 'verbose']).default('compact').describe('Response shape. "compact" (default) returns a small, predictable per-person record: name, title, linkedin_url, company {name, domain, linkedin_url, headcount}, plus seniority/email/location when the upstream provider includes them. "verbose" returns the raw provider passthrough (~30KB+ per record from FullEnrich: employment history, every company office, descriptions, skills, languages) — only use when you specifically need those fields.'),
|
|
29
30
|
reveal: z.boolean().optional().describe('Apollo-only: when true, follow the obfuscated Apollo people-search results with /apollo/people/bulk-match to reveal full names, emails, and LinkedIn URLs. Costs 1 extra credit per revealed person on top of the search credit. Has no effect for other providers (their data is already unobfuscated). Default false to protect credits.'),
|
|
30
31
|
use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_people').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
import { getSessionUsage } from '../session-usage.js'
|
|
2
|
+
import { callApi } from '../client.js'
|
|
2
3
|
|
|
3
4
|
export const getSessionUsageName = 'get_session_usage'
|
|
4
5
|
|
|
5
6
|
export const getSessionUsageDescription =
|
|
6
|
-
'Report the ColdIQ credits spent so far in
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
7
|
+
'Report the ColdIQ credits spent so far in this MCP process: total credits charged, billed-call count, ' +
|
|
8
|
+
'and a per-endpoint breakdown (highest spend first), plus the live authoritative balance for reconciliation. ' +
|
|
9
|
+
'The session tally is BEST-EFFORT — it sums what each call reported at request time and therefore: (a) cannot ' +
|
|
10
|
+
'see out-of-band async settlement (webhook refunds/charges that land after the call returns), (b) excludes ' +
|
|
11
|
+
'transport failures that never reached the server, and (c) persists for the life of the MCP process, so a ' +
|
|
12
|
+
'reused process can include earlier conversations. Treat session_spend as indicative; use authoritative_balance ' +
|
|
13
|
+
'(or get_credit_balance) as the source of truth.'
|
|
12
14
|
|
|
13
15
|
export const getSessionUsageSchema = {}
|
|
14
16
|
|
|
17
|
+
const SESSION_USAGE_NOTE =
|
|
18
|
+
'session_spend is a best-effort, header-derived tally for this MCP process; it excludes out-of-band async ' +
|
|
19
|
+
'settlement and resets only when the process restarts. Reconcile against authoritative_balance (live).'
|
|
20
|
+
|
|
15
21
|
export async function getSessionUsageHandler(_input: Record<string, unknown>) {
|
|
16
22
|
const usage = getSessionUsage()
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
// Reconcile against the authoritative balance. /me/credits is not billed, so this
|
|
25
|
+
// call never pollutes the tally. Best-effort: a failure just omits the field.
|
|
26
|
+
let authoritativeBalance: number | undefined
|
|
27
|
+
try {
|
|
28
|
+
const res = await callApi('GET', '/me/credits')
|
|
29
|
+
if (res.ok && res.data && typeof res.data === 'object') {
|
|
30
|
+
const bal = (res.data as { data?: { balance?: unknown } }).data?.balance
|
|
31
|
+
if (typeof bal === 'number') authoritativeBalance = bal
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore — reconciliation is best-effort
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
data: {
|
|
43
|
+
...usage,
|
|
44
|
+
...(authoritativeBalance !== undefined ? { authoritative_balance: authoritativeBalance } : {}),
|
|
45
|
+
note: SESSION_USAGE_NOTE,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
18
51
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { findPeopleSchema } from '../src/tools/find-people.js'
|
|
4
|
+
|
|
5
|
+
describe('find_people MCP tool schema', () => {
|
|
6
|
+
it('exposes max_per_company so the cap reaches the server (was silently dropped)', () => {
|
|
7
|
+
// Live matrix bug: the server verb honors max_per_company (D3), but the MCP tool
|
|
8
|
+
// schema didn't expose it, so Zod stripped it and the cap never applied (a
|
|
9
|
+
// multi-company search returned 5 notion contacts for max_per_company:3).
|
|
10
|
+
expect(Object.keys(findPeopleSchema)).toContain('max_per_company')
|
|
11
|
+
const parsed = z.object(findPeopleSchema).parse({
|
|
12
|
+
company_domains: ['figma.com', 'notion.so'],
|
|
13
|
+
job_titles: ['Account Executive'],
|
|
14
|
+
max_per_company: 3,
|
|
15
|
+
})
|
|
16
|
+
expect(parsed.max_per_company).toBe(3)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resetSessionUsage,
|
|
7
7
|
} from '../src/session-usage.js'
|
|
8
8
|
import { initClient, callApi } from '../src/client.js'
|
|
9
|
+
import { getSessionUsageHandler } from '../src/tools/get-session-usage.js'
|
|
9
10
|
|
|
10
11
|
describe('session-usage accumulator', () => {
|
|
11
12
|
beforeEach(() => {
|
|
@@ -128,3 +129,34 @@ describe('callApi records credit charges into session usage', () => {
|
|
|
128
129
|
expect(getSessionUsage().billedCalls).toBe(0)
|
|
129
130
|
})
|
|
130
131
|
})
|
|
132
|
+
|
|
133
|
+
// --- B2: get_session_usage reconciles against the authoritative balance ------
|
|
134
|
+
describe('get_session_usage handler reconciliation (B2)', () => {
|
|
135
|
+
const originalFetch = globalThis.fetch
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
initClient('http://test-api.local', 'test-key')
|
|
138
|
+
resetSessionUsage(0)
|
|
139
|
+
})
|
|
140
|
+
afterEach(() => { globalThis.fetch = originalFetch })
|
|
141
|
+
|
|
142
|
+
it('attaches the live authoritative_balance and a best-effort note', async () => {
|
|
143
|
+
recordCharge('/email/find', 2, 500) // header-derived tally (may drift from real balance)
|
|
144
|
+
globalThis.fetch = vi.fn(async () =>
|
|
145
|
+
new Response(JSON.stringify({ data: { balance: 480.5, usedThisMonth: 20 } }), { status: 200 }),
|
|
146
|
+
) as typeof fetch
|
|
147
|
+
|
|
148
|
+
const res = await getSessionUsageHandler({})
|
|
149
|
+
const payload = JSON.parse(res.content[0].text).data
|
|
150
|
+
expect(payload.totalCreditsCharged).toBe(2)
|
|
151
|
+
expect(payload.authoritative_balance).toBe(480.5) // reconciliation source of truth
|
|
152
|
+
expect(payload.note).toMatch(/best-effort/i)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('still returns the tally + note when the balance lookup fails (best-effort)', async () => {
|
|
156
|
+
globalThis.fetch = vi.fn(async () => new Response('nope', { status: 502 })) as typeof fetch
|
|
157
|
+
const res = await getSessionUsageHandler({})
|
|
158
|
+
const payload = JSON.parse(res.content[0].text).data
|
|
159
|
+
expect(payload.authoritative_balance).toBeUndefined()
|
|
160
|
+
expect(payload.note).toBeTruthy()
|
|
161
|
+
})
|
|
162
|
+
})
|