tapd-mcp 1.0.4 → 1.0.6
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/config-store.d.ts +54 -0
- package/dist/config-store.d.ts.map +1 -0
- package/dist/config-store.js +78 -0
- package/dist/config-store.js.map +1 -0
- package/dist/fields-cache.d.ts +33 -0
- package/dist/fields-cache.d.ts.map +1 -0
- package/dist/fields-cache.js +88 -0
- package/dist/fields-cache.js.map +1 -0
- package/dist/index.js +195 -2
- package/dist/index.js.map +1 -1
- package/dist/tapd-client.d.ts +100 -1
- package/dist/tapd-client.d.ts.map +1 -1
- package/dist/tapd-client.js +286 -5
- package/dist/tapd-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only story config sourced from the TAPD_STORY_CONFIG env var.
|
|
3
|
+
*
|
|
4
|
+
* The env var holds a JSON document with this shape:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "workspaces": {
|
|
8
|
+
* "<wsid>": {
|
|
9
|
+
* "story_defaults": {
|
|
10
|
+
* "<field-label-or-api-name>": "<value>"
|
|
11
|
+
* },
|
|
12
|
+
* "story_field_rules": {
|
|
13
|
+
* "<field-label-or-api-name>": [
|
|
14
|
+
* { "match": "题库", "value": "常规项目/题库" },
|
|
15
|
+
* { "match": "课程|财经云", "value": "常规项目/课程产品" }
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* As a convenience, callers may also pass the inner WorkspaceConfig directly
|
|
23
|
+
* (i.e. just `{ "story_defaults": ..., "story_field_rules": ... }`); it will
|
|
24
|
+
* be applied to every workspace when a per-workspace section is missing.
|
|
25
|
+
*
|
|
26
|
+
* `match` is a case-insensitive regex tested against the caller-supplied
|
|
27
|
+
* `hint` (or, when absent, the story's `name`). First match wins.
|
|
28
|
+
*/
|
|
29
|
+
export interface FieldRule {
|
|
30
|
+
match: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}
|
|
33
|
+
export interface WorkspaceConfig {
|
|
34
|
+
story_defaults?: Record<string, string>;
|
|
35
|
+
story_field_rules?: Record<string, FieldRule[]>;
|
|
36
|
+
}
|
|
37
|
+
export interface TapdUserConfig {
|
|
38
|
+
workspaces?: Record<string, WorkspaceConfig>;
|
|
39
|
+
story_defaults?: Record<string, string>;
|
|
40
|
+
story_field_rules?: Record<string, FieldRule[]>;
|
|
41
|
+
}
|
|
42
|
+
export declare function getConfigSource(): {
|
|
43
|
+
env: string;
|
|
44
|
+
raw: string | undefined;
|
|
45
|
+
};
|
|
46
|
+
export declare function readConfig(): TapdUserConfig;
|
|
47
|
+
export declare function getWorkspaceConfig(config: TapdUserConfig, workspaceId: string): WorkspaceConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Pick the first matching value from a list of rules.
|
|
50
|
+
* `match` is treated as a case-insensitive regex. Falls back to literal
|
|
51
|
+
* substring match if the regex fails to compile.
|
|
52
|
+
*/
|
|
53
|
+
export declare function pickRuleValue(rules: FieldRule[] | undefined, hint: string): string | undefined;
|
|
54
|
+
//# sourceMappingURL=config-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;CACjD;AAID,wBAAgB,eAAe,IAAI;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAE1E;AAED,wBAAgB,UAAU,IAAI,cAAc,CAS3C;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,MAAM,GAClB,eAAe,CAUjB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAY9F"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only story config sourced from the TAPD_STORY_CONFIG env var.
|
|
3
|
+
*
|
|
4
|
+
* The env var holds a JSON document with this shape:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "workspaces": {
|
|
8
|
+
* "<wsid>": {
|
|
9
|
+
* "story_defaults": {
|
|
10
|
+
* "<field-label-or-api-name>": "<value>"
|
|
11
|
+
* },
|
|
12
|
+
* "story_field_rules": {
|
|
13
|
+
* "<field-label-or-api-name>": [
|
|
14
|
+
* { "match": "题库", "value": "常规项目/题库" },
|
|
15
|
+
* { "match": "课程|财经云", "value": "常规项目/课程产品" }
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* As a convenience, callers may also pass the inner WorkspaceConfig directly
|
|
23
|
+
* (i.e. just `{ "story_defaults": ..., "story_field_rules": ... }`); it will
|
|
24
|
+
* be applied to every workspace when a per-workspace section is missing.
|
|
25
|
+
*
|
|
26
|
+
* `match` is a case-insensitive regex tested against the caller-supplied
|
|
27
|
+
* `hint` (or, when absent, the story's `name`). First match wins.
|
|
28
|
+
*/
|
|
29
|
+
const ENV_VAR = 'TAPD_STORY_CONFIG';
|
|
30
|
+
export function getConfigSource() {
|
|
31
|
+
return { env: ENV_VAR, raw: process.env[ENV_VAR] };
|
|
32
|
+
}
|
|
33
|
+
export function readConfig() {
|
|
34
|
+
const raw = process.env[ENV_VAR];
|
|
35
|
+
if (!raw || !raw.trim())
|
|
36
|
+
return {};
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getWorkspaceConfig(config, workspaceId) {
|
|
46
|
+
const ws = config.workspaces?.[workspaceId];
|
|
47
|
+
if (ws)
|
|
48
|
+
return ws;
|
|
49
|
+
if (config.story_defaults || config.story_field_rules) {
|
|
50
|
+
return {
|
|
51
|
+
story_defaults: config.story_defaults,
|
|
52
|
+
story_field_rules: config.story_field_rules,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pick the first matching value from a list of rules.
|
|
59
|
+
* `match` is treated as a case-insensitive regex. Falls back to literal
|
|
60
|
+
* substring match if the regex fails to compile.
|
|
61
|
+
*/
|
|
62
|
+
export function pickRuleValue(rules, hint) {
|
|
63
|
+
if (!rules || !hint)
|
|
64
|
+
return undefined;
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
let matched = false;
|
|
67
|
+
try {
|
|
68
|
+
matched = new RegExp(rule.match, 'i').test(hint);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
matched = hint.toLowerCase().includes(rule.match.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
if (matched)
|
|
74
|
+
return rule.value;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=config-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-store.js","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAkBH,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAEpC,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;QACjD,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAsB,EACtB,WAAmB;IAEnB,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IAClB,IAAI,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,iBAAiB,EAAE,CAAC;QACtD,OAAO;YACL,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;SAC5C,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAA8B,EAAE,IAAY;IACxE,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAClE,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IACjC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk cache for TAPD field schema (one file per workspace + entity_type).
|
|
3
|
+
*
|
|
4
|
+
* Layout: <cacheDir>/fields/<entity_type>/<workspace_id>.json
|
|
5
|
+
*
|
|
6
|
+
* cacheDir resolution: TAPD_CACHE_DIR env var → otherwise ~/.tapd-mcp/cache.
|
|
7
|
+
*
|
|
8
|
+
* The cache stores whatever the resolver feeds in (typically the parsed
|
|
9
|
+
* /stories/get_fields_info payload) plus a fetched-at timestamp; reads return
|
|
10
|
+
* null when the file is missing or unreadable so callers fall back to the API.
|
|
11
|
+
*/
|
|
12
|
+
export interface CachedFields<T = unknown> {
|
|
13
|
+
workspace_id: string;
|
|
14
|
+
entity_type: string;
|
|
15
|
+
fetched_at: string;
|
|
16
|
+
data: T;
|
|
17
|
+
}
|
|
18
|
+
export declare function getCacheDir(): string;
|
|
19
|
+
export declare function readFieldsCache<T = unknown>(workspaceId: string, entityType?: string): Promise<CachedFields<T> | null>;
|
|
20
|
+
export declare function writeFieldsCache<T = unknown>(workspaceId: string, entityType: string, data: T): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Clear cache entries.
|
|
23
|
+
* - workspaceId + entityType: remove that single file
|
|
24
|
+
* - workspaceId only: remove all entity types for that workspace
|
|
25
|
+
* - neither: remove the entire <cacheDir>/fields tree
|
|
26
|
+
*
|
|
27
|
+
* Returns the list of paths removed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function clearFieldsCache(options?: {
|
|
30
|
+
workspaceId?: string;
|
|
31
|
+
entityType?: string;
|
|
32
|
+
}): Promise<string[]>;
|
|
33
|
+
//# sourceMappingURL=fields-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields-cache.d.ts","sourceRoot":"","sources":["../src/fields-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,CAAC,CAAC;CACT;AAED,wBAAgB,WAAW,IAAI,MAAM,CAIpC;AAMD,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,WAAW,EAAE,MAAM,EACnB,UAAU,SAAU,GACnB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAWjC;AAED,wBAAsB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAChD,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,CAAC,GACN,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,CAAC,EAAE;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA2BpB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk cache for TAPD field schema (one file per workspace + entity_type).
|
|
3
|
+
*
|
|
4
|
+
* Layout: <cacheDir>/fields/<entity_type>/<workspace_id>.json
|
|
5
|
+
*
|
|
6
|
+
* cacheDir resolution: TAPD_CACHE_DIR env var → otherwise ~/.tapd-mcp/cache.
|
|
7
|
+
*
|
|
8
|
+
* The cache stores whatever the resolver feeds in (typically the parsed
|
|
9
|
+
* /stories/get_fields_info payload) plus a fetched-at timestamp; reads return
|
|
10
|
+
* null when the file is missing or unreadable so callers fall back to the API.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readFile, writeFile, rm, readdir } from 'node:fs/promises';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
export function getCacheDir() {
|
|
17
|
+
const override = process.env.TAPD_CACHE_DIR;
|
|
18
|
+
if (override && override.trim())
|
|
19
|
+
return override;
|
|
20
|
+
return join(homedir(), '.tapd-mcp', 'cache');
|
|
21
|
+
}
|
|
22
|
+
function fileFor(workspaceId, entityType) {
|
|
23
|
+
return join(getCacheDir(), 'fields', entityType, `${workspaceId}.json`);
|
|
24
|
+
}
|
|
25
|
+
export async function readFieldsCache(workspaceId, entityType = 'story') {
|
|
26
|
+
const file = fileFor(workspaceId, entityType);
|
|
27
|
+
if (!existsSync(file))
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const text = await readFile(file, 'utf8');
|
|
31
|
+
const parsed = JSON.parse(text);
|
|
32
|
+
if (parsed && parsed.data)
|
|
33
|
+
return parsed;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function writeFieldsCache(workspaceId, entityType, data) {
|
|
41
|
+
const file = fileFor(workspaceId, entityType);
|
|
42
|
+
await mkdir(dirname(file), { recursive: true });
|
|
43
|
+
const payload = {
|
|
44
|
+
workspace_id: workspaceId,
|
|
45
|
+
entity_type: entityType,
|
|
46
|
+
fetched_at: new Date().toISOString(),
|
|
47
|
+
data,
|
|
48
|
+
};
|
|
49
|
+
await writeFile(file, JSON.stringify(payload, null, 2), 'utf8');
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear cache entries.
|
|
54
|
+
* - workspaceId + entityType: remove that single file
|
|
55
|
+
* - workspaceId only: remove all entity types for that workspace
|
|
56
|
+
* - neither: remove the entire <cacheDir>/fields tree
|
|
57
|
+
*
|
|
58
|
+
* Returns the list of paths removed.
|
|
59
|
+
*/
|
|
60
|
+
export async function clearFieldsCache(options) {
|
|
61
|
+
const root = join(getCacheDir(), 'fields');
|
|
62
|
+
if (!existsSync(root))
|
|
63
|
+
return [];
|
|
64
|
+
if (options?.workspaceId && options.entityType) {
|
|
65
|
+
const file = fileFor(options.workspaceId, options.entityType);
|
|
66
|
+
if (!existsSync(file))
|
|
67
|
+
return [];
|
|
68
|
+
await rm(file);
|
|
69
|
+
return [file];
|
|
70
|
+
}
|
|
71
|
+
if (options?.workspaceId) {
|
|
72
|
+
const removed = [];
|
|
73
|
+
const types = await readdir(root, { withFileTypes: true });
|
|
74
|
+
for (const t of types) {
|
|
75
|
+
if (!t.isDirectory())
|
|
76
|
+
continue;
|
|
77
|
+
const file = join(root, t.name, `${options.workspaceId}.json`);
|
|
78
|
+
if (existsSync(file)) {
|
|
79
|
+
await rm(file);
|
|
80
|
+
removed.push(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
await rm(root, { recursive: true, force: true });
|
|
86
|
+
return [root];
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=fields-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields-cache.js","sourceRoot":"","sources":["../src/fields-cache.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
10
|
import { TapdClient } from './tapd-client.js';
|
|
11
|
+
import { readConfig, getConfigSource, } from './config-store.js';
|
|
11
12
|
const API_TOKEN = process.env.TAPD_API_TOKEN;
|
|
12
13
|
const DEFAULT_WORKSPACE_ID = process.env.TAPD_WORKSPACE_ID;
|
|
13
14
|
const CURRENT_USER = process.env.TAPD_CURRENT_USER;
|
|
@@ -109,6 +110,35 @@ const tools = [
|
|
|
109
110
|
required: ['workspace_id', 'story_id'],
|
|
110
111
|
},
|
|
111
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: 'tapd_set_story_custom_field',
|
|
115
|
+
description: 'Set a story custom field by user-facing name (e.g. "提测时间") or by API name ' +
|
|
116
|
+
'(e.g. "custom_field_one"). Resolves the name against the workspace\'s custom-field ' +
|
|
117
|
+
'definitions before writing, and returns the previous value. ' +
|
|
118
|
+
'`field` accepts a comma-separated alias list — the first one that resolves wins, ' +
|
|
119
|
+
'so `field="提测时间,提测日期,TestDate"` covers labels that vary across workspaces. ' +
|
|
120
|
+
'Pass `refresh=true` after a workspace admin edits the field config to bypass the ' +
|
|
121
|
+
'on-disk schema cache (~/.tapd-mcp/cache or $TAPD_CACHE_DIR).',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
workspace_id: { type: 'string', description: 'Workspace ID' },
|
|
126
|
+
story_id: { type: 'string', description: 'Story ID (required unless `url` is provided)' },
|
|
127
|
+
url: { type: 'string', description: 'TAPD story URL (alternative to workspace_id + story_id)' },
|
|
128
|
+
field: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Field to write — a label like "提测时间" / "TestDate" or an API name like ' +
|
|
131
|
+
'"custom_field_one". Comma-separated for fuzzy match; first hit wins.',
|
|
132
|
+
},
|
|
133
|
+
value: { type: 'string', description: 'New value to write' },
|
|
134
|
+
refresh: {
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
description: 'Drop the field-schema cache for this workspace before resolving (default false).',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ['field', 'value'],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
112
142
|
// ==================== Task Tools ====================
|
|
113
143
|
{
|
|
114
144
|
name: 'tapd_get_task',
|
|
@@ -511,7 +541,10 @@ const tools = [
|
|
|
511
541
|
// ==================== Custom Fields Settings Tool ====================
|
|
512
542
|
{
|
|
513
543
|
name: 'tapd_get_custom_fields_settings',
|
|
514
|
-
description: 'Get custom field schema / definitions for stories (or other entry types) in a workspace.
|
|
544
|
+
description: 'Get custom field schema / definitions for stories (or other entry types) in a workspace. ' +
|
|
545
|
+
'Useful for discovering available custom field names and their types/options before reading ' +
|
|
546
|
+
'or writing custom fields. Pass `refresh=true` to drop the on-disk cache before reading ' +
|
|
547
|
+
'(use after a workspace admin changes labels / adds fields).',
|
|
515
548
|
inputSchema: {
|
|
516
549
|
type: 'object',
|
|
517
550
|
properties: {
|
|
@@ -520,11 +553,94 @@ const tools = [
|
|
|
520
553
|
type: 'string',
|
|
521
554
|
description: 'Entry type to query custom fields for. Defaults to "story".',
|
|
522
555
|
},
|
|
556
|
+
refresh: {
|
|
557
|
+
type: 'boolean',
|
|
558
|
+
description: 'Drop the field-schema cache for this workspace + entry_type before reading.',
|
|
559
|
+
},
|
|
523
560
|
},
|
|
524
561
|
required: ['workspace_id'],
|
|
525
562
|
},
|
|
526
563
|
},
|
|
564
|
+
// ==================== Per-user config ====================
|
|
565
|
+
{
|
|
566
|
+
name: 'tapd_get_config',
|
|
567
|
+
description: 'Read the story config sourced from the TAPD_STORY_CONFIG environment variable. ' +
|
|
568
|
+
'Returns the parsed config plus the env var name. ' +
|
|
569
|
+
'Pass `workspace_id` to scope the response to one workspace.',
|
|
570
|
+
inputSchema: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
properties: {
|
|
573
|
+
workspace_id: { type: 'string', description: 'Optional: only return that workspace section' },
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'tapd_apply_story_defaults',
|
|
579
|
+
description: 'Apply the per-workspace story defaults + matched field rules from $TAPD_STORY_CONFIG ' +
|
|
580
|
+
'to a single story. Use after tapd_create_story to populate attribution fields ' +
|
|
581
|
+
'(需求类型 / 项目归属 / 成本归属, etc.) without hard-coding them in the agent. ' +
|
|
582
|
+
'Caller-supplied `overrides` always win over config; cascade fields are retried on a second pass ' +
|
|
583
|
+
'so parent/child write order is automatic. Set `dry_run=true` to preview without writing.',
|
|
584
|
+
inputSchema: {
|
|
585
|
+
type: 'object',
|
|
586
|
+
properties: {
|
|
587
|
+
workspace_id: { type: 'string', description: 'Workspace ID (falls back to TAPD_WORKSPACE_ID)' },
|
|
588
|
+
story_id: { type: 'string', description: 'Story ID (required unless `url` is provided)' },
|
|
589
|
+
url: { type: 'string', description: 'TAPD story URL (alternative to workspace_id + story_id)' },
|
|
590
|
+
hint: {
|
|
591
|
+
type: 'string',
|
|
592
|
+
description: 'Text used to evaluate field rules (e.g. "题库后台增加批量导入"). Defaults to the story name when omitted.',
|
|
593
|
+
},
|
|
594
|
+
overrides: {
|
|
595
|
+
type: 'object',
|
|
596
|
+
description: 'Field -> value pairs that override both defaults and rules.',
|
|
597
|
+
additionalProperties: { type: 'string' },
|
|
598
|
+
},
|
|
599
|
+
dry_run: { type: 'boolean', description: 'Preview the plan without writing' },
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: 'tapd_get_story_changes',
|
|
605
|
+
description: 'Get the change history of a story (who changed which field, when, old → new value). ' +
|
|
606
|
+
'Backed by /story_changes. Useful for audit trails and figuring out who flipped a status or ' +
|
|
607
|
+
'rewrote the description.',
|
|
608
|
+
inputSchema: {
|
|
609
|
+
type: 'object',
|
|
610
|
+
properties: {
|
|
611
|
+
workspace_id: { type: 'string', description: 'Workspace ID' },
|
|
612
|
+
story_id: { type: 'string', description: 'Story ID' },
|
|
613
|
+
limit: { type: 'number', description: 'Max results (default 20)' },
|
|
614
|
+
page: { type: 'number', description: 'Page number (default 1)' },
|
|
615
|
+
},
|
|
616
|
+
required: ['story_id'],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'tapd_list_workspace_users',
|
|
621
|
+
description: 'List members of a workspace via /users/get_users_by_workspace_id. ' +
|
|
622
|
+
'Use this to look up English user ids (the only form TAPD accepts for owner / current_owner / timesheet owner) ' +
|
|
623
|
+
'when the user only knows a Chinese display name.',
|
|
624
|
+
inputSchema: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
properties: {
|
|
627
|
+
workspace_id: { type: 'string', description: 'Workspace ID (falls back to TAPD_WORKSPACE_ID)' },
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
},
|
|
527
631
|
// ==================== Release Tools ====================
|
|
632
|
+
{
|
|
633
|
+
name: 'tapd_get_release',
|
|
634
|
+
description: 'Get details of a specific release by ID',
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: 'object',
|
|
637
|
+
properties: {
|
|
638
|
+
workspace_id: { type: 'string', description: 'Workspace ID' },
|
|
639
|
+
release_id: { type: 'string', description: 'Release ID' },
|
|
640
|
+
},
|
|
641
|
+
required: ['release_id'],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
528
644
|
{
|
|
529
645
|
name: 'tapd_list_releases',
|
|
530
646
|
description: 'List releases in a workspace',
|
|
@@ -700,6 +816,32 @@ async function handleToolCall(name, args) {
|
|
|
700
816
|
due: args.due,
|
|
701
817
|
});
|
|
702
818
|
}
|
|
819
|
+
case 'tapd_set_story_custom_field': {
|
|
820
|
+
const field = args.field;
|
|
821
|
+
const value = args.value;
|
|
822
|
+
if (!field)
|
|
823
|
+
throw new Error('field is required');
|
|
824
|
+
if (value === undefined || value === null)
|
|
825
|
+
throw new Error('value is required');
|
|
826
|
+
let workspaceId;
|
|
827
|
+
let storyId;
|
|
828
|
+
if (args.url) {
|
|
829
|
+
const parsed = client.parseUrl(args.url);
|
|
830
|
+
if (!parsed)
|
|
831
|
+
throw new Error('Invalid TAPD URL');
|
|
832
|
+
workspaceId = parsed.workspaceId;
|
|
833
|
+
storyId = parsed.resourceId;
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
workspaceId = getWorkspaceId(args);
|
|
837
|
+
storyId = args.story_id;
|
|
838
|
+
if (!storyId)
|
|
839
|
+
throw new Error('story_id (or url) is required');
|
|
840
|
+
}
|
|
841
|
+
return await client.setStoryCustomField(workspaceId, storyId, field, value, {
|
|
842
|
+
refresh: args.refresh === true,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
703
845
|
// Task handlers
|
|
704
846
|
case 'tapd_get_task': {
|
|
705
847
|
if (args.url) {
|
|
@@ -901,9 +1043,60 @@ async function handleToolCall(name, args) {
|
|
|
901
1043
|
}
|
|
902
1044
|
// Custom fields settings handler
|
|
903
1045
|
case 'tapd_get_custom_fields_settings': {
|
|
904
|
-
return await client.getCustomFieldsSettings(getWorkspaceId(args), args.entry_type || 'story');
|
|
1046
|
+
return await client.getCustomFieldsSettings(getWorkspaceId(args), args.entry_type || 'story', { refresh: args.refresh === true });
|
|
1047
|
+
}
|
|
1048
|
+
case 'tapd_get_config': {
|
|
1049
|
+
const config = readConfig();
|
|
1050
|
+
const { env, raw } = getConfigSource();
|
|
1051
|
+
const source = { env, configured: Boolean(raw && raw.trim()) };
|
|
1052
|
+
if (args.workspace_id) {
|
|
1053
|
+
const wsid = args.workspace_id;
|
|
1054
|
+
const ws = config.workspaces?.[wsid] ??
|
|
1055
|
+
(config.story_defaults || config.story_field_rules
|
|
1056
|
+
? {
|
|
1057
|
+
story_defaults: config.story_defaults,
|
|
1058
|
+
story_field_rules: config.story_field_rules,
|
|
1059
|
+
}
|
|
1060
|
+
: {});
|
|
1061
|
+
return { source, workspace_id: wsid, config: ws };
|
|
1062
|
+
}
|
|
1063
|
+
return { source, config };
|
|
1064
|
+
}
|
|
1065
|
+
case 'tapd_apply_story_defaults': {
|
|
1066
|
+
let workspaceId;
|
|
1067
|
+
let storyId;
|
|
1068
|
+
if (args.url) {
|
|
1069
|
+
const parsed = client.parseUrl(args.url);
|
|
1070
|
+
if (!parsed)
|
|
1071
|
+
throw new Error('Invalid TAPD URL');
|
|
1072
|
+
workspaceId = parsed.workspaceId;
|
|
1073
|
+
storyId = parsed.resourceId;
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
workspaceId = getWorkspaceId(args);
|
|
1077
|
+
storyId = args.story_id;
|
|
1078
|
+
if (!storyId)
|
|
1079
|
+
throw new Error('story_id (or url) is required');
|
|
1080
|
+
}
|
|
1081
|
+
return await client.applyStoryDefaults(workspaceId, storyId, {
|
|
1082
|
+
hint: args.hint,
|
|
1083
|
+
overrides: args.overrides ?? undefined,
|
|
1084
|
+
dryRun: args.dry_run === true,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
case 'tapd_get_story_changes': {
|
|
1088
|
+
return await client.getStoryChanges(getWorkspaceId(args), args.story_id, {
|
|
1089
|
+
limit: args.limit,
|
|
1090
|
+
page: args.page,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
case 'tapd_list_workspace_users': {
|
|
1094
|
+
return await client.getWorkspaceUsers(getWorkspaceId(args));
|
|
905
1095
|
}
|
|
906
1096
|
// Release handlers
|
|
1097
|
+
case 'tapd_get_release': {
|
|
1098
|
+
return await client.getRelease(getWorkspaceId(args), args.release_id);
|
|
1099
|
+
}
|
|
907
1100
|
case 'tapd_list_releases': {
|
|
908
1101
|
return await client.listReleases(getWorkspaceId(args), {
|
|
909
1102
|
status: args.status,
|