@tymio/mcp-server 1.0.1 → 2.0.0
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/CHANGELOG.md +30 -0
- package/README.md +27 -0
- package/TYMIO_MCP_CLI_AGENT_GUIDANCE.md +1 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +12 -1
- package/dist/apiKeyStdio.js +130 -74
- package/dist/apiKeyTenantResolve.d.ts +4 -0
- package/dist/apiKeyTenantResolve.js +17 -0
- package/dist/cli.js +5 -0
- package/dist/cliMessages.d.ts +1 -1
- package/dist/cliMessages.js +4 -0
- package/dist/hubProxyStdio.js +7 -2
- package/dist/persona.d.ts +16 -0
- package/dist/persona.js +93 -0
- package/dist/stdioHints.js +7 -2
- package/dist/workspaceSlug.d.ts +13 -0
- package/dist/workspaceSlug.js +53 -0
- package/package.json +4 -2
- package/personas/dev.md +33 -0
- package/personas/pm.md +31 -0
- package/personas/po.md +35 -0
- package/personas/workspace.md +41 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Pinned workspace for stdio** — `tymio-mcp` (OAuth proxy and API-key bridge) **requires** `TYMIO_WORKSPACE_SLUG` or `DRD_WORKSPACE_SLUG` (the hub workspace slug this process is bound to). Every tool call must include **`workspaceSlug`** matching that pin (case-insensitive). Prevents agents from targeting another workspace for the same user.
|
|
8
|
+
- **Tests / local tooling only** — set `TYMIO_MCP_SKIP_WORKSPACE_PINNING=1` to skip the startup requirement (do not use in production agent configs).
|
|
9
|
+
|
|
10
|
+
### Added / changed
|
|
11
|
+
|
|
12
|
+
- **API-key stdio** — Resolves the pinned slug to a tenant via `GET /api/me/tenants` and sends **`X-Tenant-Id`** on all hub REST calls; tool payloads still carry `workspaceSlug` for consistency but must match the pin.
|
|
13
|
+
- **OAuth stdio (hub proxy)** — Asserts tool arguments match the pinned slug before `callTool` to the hosted MCP.
|
|
14
|
+
- **Hub (server) MCP** — Stricter `workspaceSlug` validation (length, `^[a-z0-9-]+$`) and **case-insensitive** match to the session workspace; API-key sessions can use tenant-list routes needed for resolution (`authViaApiKey` + `requireSession` behavior).
|
|
15
|
+
|
|
16
|
+
### Requires
|
|
17
|
+
|
|
18
|
+
- **Deploy hub** with the matching server changes **before or with** rolling out this CLI to users who rely on API-key stdio or the updated MCP slug rules.
|
|
19
|
+
|
|
20
|
+
## 1.1.0
|
|
21
|
+
|
|
22
|
+
- **Bundled agent personas** (`personas/*.md`): `workspace`, `pm`, `po`, `dev` — aligned with Tymio hub roles; shipped in the npm tarball.
|
|
23
|
+
- **`tymio-mcp persona list`** and **`tymio-mcp persona <id>`** — print persona Markdown to stdout (or list ids).
|
|
24
|
+
- **`TYMIO_MCP_PERSONA`** — optional env on the stdio process; appends the selected persona to MCP server **`instructions`** after the main agent guide (`hub` aliases `workspace`). Invalid values log a stderr warning and fall back to the base guide only.
|
|
25
|
+
- **Startup stderr hint** — when a valid persona is set, reminds that instructions include it and how to print the prompt (`tymio-mcp persona <id>`).
|
|
26
|
+
- **Agent guidance** — `TYMIO_MCP_CLI_AGENT_GUIDANCE.md` updated with persona usage; `README.md` and help text document commands and env.
|
|
27
|
+
|
|
28
|
+
## 1.0.1
|
|
29
|
+
|
|
30
|
+
Prior release (OAuth proxy, API-key REST subset, `tymio-mcp instructions`, login/logout).
|
package/README.md
CHANGED
|
@@ -37,6 +37,30 @@ Installable **`tymio-mcp`** command: connect editors and agents to **Tymio** in
|
|
|
37
37
|
|
|
38
38
|
**Agents / IDE:** MCP clients that support [server instructions](https://modelcontextprotocol.io) receive the same long-form guide as `tymio-mcp instructions` during the initialize handshake. You can still run `tymio-mcp instructions` in a terminal to print it, or read this README.
|
|
39
39
|
|
|
40
|
+
### Bundled agent personas (PM / PO / DEV)
|
|
41
|
+
|
|
42
|
+
The package ships Markdown prompts in **`personas/`** (aligned with Cursor Skills in the monorepo).
|
|
43
|
+
|
|
44
|
+
| Mechanism | What it does |
|
|
45
|
+
|-----------|----------------|
|
|
46
|
+
| **`tymio-mcp persona list`** | Lists persona ids and usage |
|
|
47
|
+
| **`tymio-mcp persona pm`** (or `po`, `dev`, `workspace`) | Prints that prompt to **stdout** (pipe into docs or paste into a chat) |
|
|
48
|
+
| **`TYMIO_MCP_PERSONA=pm`** on the `tymio-mcp` process | **Appends** the same Markdown to MCP server **`instructions`** after the main CLI guide — steers the model for clients that honor instructions (no Skills required). Use `hub` as an alias for `workspace`. |
|
|
49
|
+
|
|
50
|
+
Example Cursor stdio config with a Product Owner bias:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"tymio-po": {
|
|
56
|
+
"command": "tymio-mcp",
|
|
57
|
+
"args": [],
|
|
58
|
+
"env": { "TYMIO_MCP_PERSONA": "po" }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
40
64
|
### OAuth callback port
|
|
41
65
|
|
|
42
66
|
The CLI listens on **`http://127.0.0.1:19876/callback`** during `login` (override with `TYMIO_OAUTH_PORT`). That URI must be reachable from your browser and should stay stable so it matches the dynamically registered OAuth client.
|
|
@@ -84,6 +108,9 @@ Example:
|
|
|
84
108
|
| `tymio-mcp` | Run stdio MCP (OAuth proxy unless API key env is set) |
|
|
85
109
|
| `tymio-mcp login [url]` | OAuth sign-in; optional MCP URL overrides `TYMIO_MCP_URL` |
|
|
86
110
|
| `tymio-mcp logout` | Delete stored OAuth client + tokens |
|
|
111
|
+
| `tymio-mcp instructions` / `guide` | Print full agent Markdown (same as MCP `instructions` base) |
|
|
112
|
+
| `tymio-mcp persona list` | Bundled persona ids (`pm`, `po`, `dev`, `workspace`) |
|
|
113
|
+
| `tymio-mcp persona <id>` | Print one persona Markdown to stdout |
|
|
87
114
|
| `tymio-mcp help` | Usage |
|
|
88
115
|
|
|
89
116
|
---
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
3. **`DRD_API_KEY` / `API_KEY` on the stdio process** means the **server deployment automation secret** (the same value as the hub’s configured `API_KEY` for `Authorization: Bearer …` on REST). It is **not** something each user generates in the Tymio UI. Only operators with access to deployment secrets use it (CI, scripts, optional stdio “REST bridge” mode).
|
|
10
10
|
4. **Default vs API-key stdio:** If `DRD_API_KEY` or `API_KEY` is set to a non-empty value in the **environment of the `tymio-mcp` process**, the CLI uses a **fixed REST tool subset**, not OAuth to the hosted hub. For the **full** tool surface, use **remote** `…/mcp` or stdio **without** those env vars (after `tymio-mcp login`).
|
|
11
11
|
5. **Full text of this guide:** shell command `tymio-mcp instructions` (or `tymio-mcp guide`). MCP clients that support server `instructions` receive this content at initialize when using the published CLI.
|
|
12
|
+
6. **Bundled agent personas (PM / PO / DEV / workspace):** optional Markdown prompts ship with the package under `personas/`. **Print a prompt:** `tymio-mcp persona pm` (or `po`, `dev`, `workspace`). **Embed in MCP `instructions`:** set `TYMIO_MCP_PERSONA=pm` (same ids; `hub` aliases `workspace`) on the `tymio-mcp` process — the stdio server appends that persona after this guide so clients that honor `instructions` steer the model without Cursor Skills. **List ids:** `tymio-mcp persona list`.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal Tymio hub API client for the stdio MCP server. Uses DRD_API_BASE_URL and DRD_API_KEY from env.
|
|
3
3
|
*/
|
|
4
|
+
export declare function setApiKeyBridgeTenantId(tenantId: string): void;
|
|
5
|
+
export declare function clearApiKeyBridgeTenant(): void;
|
|
4
6
|
/** JSON-friendly body; plain objects are stringified. */
|
|
5
7
|
export type DrdFetchInit = Omit<RequestInit, "body"> & {
|
|
6
8
|
body?: string | Record<string, unknown>;
|
package/dist/api.js
CHANGED
|
@@ -4,8 +4,19 @@
|
|
|
4
4
|
/** Hub origin (no `/mcp` path). Stdio bridge calls REST under `/api/...`. */
|
|
5
5
|
const baseUrl = process.env.DRD_API_BASE_URL ?? "https://tymio.app";
|
|
6
6
|
const apiKey = process.env.DRD_API_KEY ?? process.env.API_KEY ?? "";
|
|
7
|
+
/** Set by API-key stdio after resolving slug → tenant id (never send cross-tenant requests). */
|
|
8
|
+
let bridgeTenantHeaders = {};
|
|
9
|
+
export function setApiKeyBridgeTenantId(tenantId) {
|
|
10
|
+
bridgeTenantHeaders = { "X-Tenant-Id": tenantId };
|
|
11
|
+
}
|
|
12
|
+
export function clearApiKeyBridgeTenant() {
|
|
13
|
+
bridgeTenantHeaders = {};
|
|
14
|
+
}
|
|
7
15
|
function headers() {
|
|
8
|
-
const h = {
|
|
16
|
+
const h = {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
...bridgeTenantHeaders
|
|
19
|
+
};
|
|
9
20
|
if (apiKey)
|
|
10
21
|
h["Authorization"] = `Bearer ${apiKey}`;
|
|
11
22
|
return h;
|
package/dist/apiKeyStdio.js
CHANGED
|
@@ -1,75 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* REST/API-key stdio bridge (subset of hub tools). Set DRD_API_BASE_URL + DRD_API_KEY (or API_KEY).
|
|
3
|
+
* Requires TYMIO_WORKSPACE_SLUG or DRD_WORKSPACE_SLUG (unless TYMIO_MCP_SKIP_WORKSPACE_PINNING=1 for tests).
|
|
3
4
|
*/
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { resolveTenantIdForWorkspaceSlug } from "./apiKeyTenantResolve.js";
|
|
9
|
+
import { drdFetch, drdFetchText, getBaseUrl, hasApiKey, setApiKeyBridgeTenantId } from "./api.js";
|
|
10
|
+
import { getMcpServerInstructions } from "./persona.js";
|
|
9
11
|
import { toolTextWithFeedback } from "./mcpFeedbackFooter.js";
|
|
10
12
|
import { writeStdioStartupHint } from "./stdioHints.js";
|
|
13
|
+
import { assertToolArgsMatchPinnedWorkspace, omitWorkspaceSlug, readPinnedWorkspaceSlugForStdio, WORKSPACE_SLUG_ZOD } from "./workspaceSlug.js";
|
|
11
14
|
export async function runApiKeyStdio() {
|
|
12
15
|
writeStdioStartupHint("api-key");
|
|
13
|
-
const
|
|
16
|
+
const pinnedSlug = readPinnedWorkspaceSlugForStdio();
|
|
17
|
+
if (pinnedSlug) {
|
|
18
|
+
try {
|
|
19
|
+
const tenantId = await resolveTenantIdForWorkspaceSlug(pinnedSlug);
|
|
20
|
+
setApiKeyBridgeTenantId(tenantId);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
process.stderr.write(`[tymio-mcp] API-key bridge: cannot resolve workspace: ${e}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const server = new McpServer({ name: "tymio-hub", version: "1.0.0" }, { instructions: getMcpServerInstructions() });
|
|
14
28
|
async function textContent(text) {
|
|
15
29
|
return toolTextWithFeedback(getBaseUrl(), text);
|
|
16
30
|
}
|
|
17
|
-
|
|
31
|
+
function assertPin(args, tool) {
|
|
32
|
+
if (pinnedSlug)
|
|
33
|
+
assertToolArgsMatchPinnedWorkspace(args, pinnedSlug, tool);
|
|
34
|
+
}
|
|
35
|
+
const ws = { workspaceSlug: WORKSPACE_SLUG_ZOD };
|
|
18
36
|
server.registerTool("drd_health", {
|
|
19
37
|
title: "Tymio API health check",
|
|
20
|
-
description: "Check if the Tymio hub API is reachable.",
|
|
21
|
-
inputSchema: z.object(
|
|
22
|
-
}, async () => {
|
|
38
|
+
description: "Check if the Tymio hub API is reachable. Requires workspaceSlug (must match server pin).",
|
|
39
|
+
inputSchema: z.object(ws)
|
|
40
|
+
}, async (args) => {
|
|
41
|
+
assertPin(args, "drd_health");
|
|
23
42
|
const data = await drdFetch("/api/health");
|
|
24
43
|
return textContent(JSON.stringify(data));
|
|
25
44
|
});
|
|
26
45
|
server.registerTool("drd_meta", {
|
|
27
46
|
title: "Get Tymio meta",
|
|
28
47
|
description: "Get meta data: domains, products, accounts, partners, personas, revenue streams, users.",
|
|
29
|
-
inputSchema: z.object(
|
|
30
|
-
}, async () => {
|
|
48
|
+
inputSchema: z.object(ws)
|
|
49
|
+
}, async (args) => {
|
|
50
|
+
assertPin(args, "drd_meta");
|
|
31
51
|
const data = await drdFetch("/api/meta");
|
|
32
52
|
return textContent(JSON.stringify(data, null, 2));
|
|
33
53
|
});
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
const listInitiativesSchema = z
|
|
55
|
+
.object({
|
|
36
56
|
domainId: z.string().optional(),
|
|
37
57
|
ownerId: z.string().optional(),
|
|
38
58
|
horizon: z.enum(["NOW", "NEXT", "LATER"]).optional(),
|
|
39
59
|
priority: z.enum(["P0", "P1", "P2", "P3"]).optional(),
|
|
40
60
|
isGap: z.boolean().optional()
|
|
41
|
-
})
|
|
61
|
+
})
|
|
62
|
+
.extend(ws);
|
|
42
63
|
server.registerTool("drd_list_initiatives", {
|
|
43
64
|
title: "List initiatives",
|
|
44
65
|
description: "List initiatives with optional filters: domainId, ownerId, horizon, priority, isGap.",
|
|
45
66
|
inputSchema: listInitiativesSchema
|
|
46
67
|
}, async (args) => {
|
|
68
|
+
assertPin(args, "drd_list_initiatives");
|
|
69
|
+
const { workspaceSlug: _w, ...filters } = args;
|
|
47
70
|
const params = new URLSearchParams();
|
|
48
|
-
if (
|
|
49
|
-
params.set("domainId",
|
|
50
|
-
if (
|
|
51
|
-
params.set("ownerId",
|
|
52
|
-
if (
|
|
53
|
-
params.set("horizon",
|
|
54
|
-
if (
|
|
55
|
-
params.set("priority",
|
|
56
|
-
if (
|
|
57
|
-
params.set("isGap", String(
|
|
71
|
+
if (filters.domainId)
|
|
72
|
+
params.set("domainId", filters.domainId);
|
|
73
|
+
if (filters.ownerId)
|
|
74
|
+
params.set("ownerId", filters.ownerId);
|
|
75
|
+
if (filters.horizon)
|
|
76
|
+
params.set("horizon", filters.horizon);
|
|
77
|
+
if (filters.priority)
|
|
78
|
+
params.set("priority", filters.priority);
|
|
79
|
+
if (filters.isGap !== undefined)
|
|
80
|
+
params.set("isGap", String(filters.isGap));
|
|
58
81
|
const data = await drdFetch(`/api/initiatives?${params.toString()}`);
|
|
59
82
|
return textContent(JSON.stringify(data.initiatives, null, 2));
|
|
60
83
|
});
|
|
61
84
|
server.registerTool("drd_get_initiative", {
|
|
62
85
|
title: "Get initiative by ID",
|
|
63
86
|
description: "Get a single initiative by its ID.",
|
|
64
|
-
inputSchema: z.object({ id: z.string().describe("Initiative ID") })
|
|
65
|
-
}, async (
|
|
87
|
+
inputSchema: z.object({ id: z.string().describe("Initiative ID") }).extend(ws)
|
|
88
|
+
}, async (args) => {
|
|
89
|
+
assertPin(args, "drd_get_initiative");
|
|
90
|
+
const { id } = omitWorkspaceSlug(args);
|
|
66
91
|
const data = await drdFetch(`/api/initiatives/${id}`);
|
|
67
92
|
return textContent(JSON.stringify(data.initiative, null, 2));
|
|
68
93
|
});
|
|
69
94
|
server.registerTool("drd_create_initiative", {
|
|
70
95
|
title: "Create initiative",
|
|
71
96
|
description: "Create a new initiative. Requires admin/editor role.",
|
|
72
|
-
inputSchema: z
|
|
97
|
+
inputSchema: z
|
|
98
|
+
.object({
|
|
73
99
|
title: z.string(),
|
|
74
100
|
domainId: z.string(),
|
|
75
101
|
description: z.string().optional(),
|
|
@@ -80,7 +106,10 @@ export async function runApiKeyStdio() {
|
|
|
80
106
|
commercialType: z.string().optional(),
|
|
81
107
|
isGap: z.boolean().optional()
|
|
82
108
|
})
|
|
83
|
-
|
|
109
|
+
.extend(ws)
|
|
110
|
+
}, async (args) => {
|
|
111
|
+
assertPin(args, "drd_create_initiative");
|
|
112
|
+
const body = omitWorkspaceSlug(args);
|
|
84
113
|
const data = await drdFetch("/api/initiatives", {
|
|
85
114
|
method: "POST",
|
|
86
115
|
body: JSON.stringify(body)
|
|
@@ -90,7 +119,8 @@ export async function runApiKeyStdio() {
|
|
|
90
119
|
server.registerTool("drd_update_initiative", {
|
|
91
120
|
title: "Update initiative",
|
|
92
121
|
description: "Update an existing initiative by ID.",
|
|
93
|
-
inputSchema: z
|
|
122
|
+
inputSchema: z
|
|
123
|
+
.object({
|
|
94
124
|
id: z.string(),
|
|
95
125
|
title: z.string().optional(),
|
|
96
126
|
domainId: z.string().optional(),
|
|
@@ -102,7 +132,10 @@ export async function runApiKeyStdio() {
|
|
|
102
132
|
commercialType: z.string().optional(),
|
|
103
133
|
isGap: z.boolean().optional()
|
|
104
134
|
})
|
|
105
|
-
|
|
135
|
+
.extend(ws)
|
|
136
|
+
}, async (args) => {
|
|
137
|
+
assertPin(args, "drd_update_initiative");
|
|
138
|
+
const { id, ...body } = omitWorkspaceSlug(args);
|
|
106
139
|
const data = await drdFetch(`/api/initiatives/${id}`, {
|
|
107
140
|
method: "PUT",
|
|
108
141
|
body: JSON.stringify(body)
|
|
@@ -112,29 +145,35 @@ export async function runApiKeyStdio() {
|
|
|
112
145
|
server.registerTool("drd_delete_initiative", {
|
|
113
146
|
title: "Delete initiative",
|
|
114
147
|
description: "Delete an initiative by ID.",
|
|
115
|
-
inputSchema: z.object({ id: z.string() })
|
|
116
|
-
}, async (
|
|
148
|
+
inputSchema: z.object({ id: z.string() }).extend(ws)
|
|
149
|
+
}, async (args) => {
|
|
150
|
+
assertPin(args, "drd_delete_initiative");
|
|
151
|
+
const { id } = omitWorkspaceSlug(args);
|
|
117
152
|
await drdFetch(`/api/initiatives/${id}`, { method: "DELETE" });
|
|
118
153
|
return textContent(JSON.stringify({ ok: true }));
|
|
119
154
|
});
|
|
120
|
-
// --- Domains, products, personas
|
|
121
155
|
server.registerTool("drd_list_domains", {
|
|
122
156
|
title: "List domains",
|
|
123
157
|
description: "List all domains.",
|
|
124
|
-
inputSchema: z.object(
|
|
125
|
-
}, async () => {
|
|
158
|
+
inputSchema: z.object(ws)
|
|
159
|
+
}, async (args) => {
|
|
160
|
+
assertPin(args, "drd_list_domains");
|
|
126
161
|
const data = await drdFetch("/api/domains");
|
|
127
162
|
return textContent(JSON.stringify(data.domains, null, 2));
|
|
128
163
|
});
|
|
129
164
|
server.registerTool("drd_create_domain", {
|
|
130
165
|
title: "Create domain",
|
|
131
166
|
description: "Create a new domain (pillar). Requires workspace OWNER or ADMIN.",
|
|
132
|
-
inputSchema: z
|
|
167
|
+
inputSchema: z
|
|
168
|
+
.object({
|
|
133
169
|
name: z.string().min(1),
|
|
134
170
|
color: z.string().min(1),
|
|
135
171
|
sortOrder: z.number().int().optional()
|
|
136
172
|
})
|
|
137
|
-
|
|
173
|
+
.extend(ws)
|
|
174
|
+
}, async (args) => {
|
|
175
|
+
assertPin(args, "drd_create_domain");
|
|
176
|
+
const body = omitWorkspaceSlug(args);
|
|
138
177
|
const data = await drdFetch("/api/domains", {
|
|
139
178
|
method: "POST",
|
|
140
179
|
body: JSON.stringify({
|
|
@@ -148,107 +187,120 @@ export async function runApiKeyStdio() {
|
|
|
148
187
|
server.registerTool("drd_list_products", {
|
|
149
188
|
title: "List products",
|
|
150
189
|
description: "List all products (with hierarchy).",
|
|
151
|
-
inputSchema: z.object(
|
|
152
|
-
}, async () => {
|
|
190
|
+
inputSchema: z.object(ws)
|
|
191
|
+
}, async (args) => {
|
|
192
|
+
assertPin(args, "drd_list_products");
|
|
153
193
|
const data = await drdFetch("/api/products");
|
|
154
194
|
return textContent(JSON.stringify(data.products, null, 2));
|
|
155
195
|
});
|
|
156
196
|
server.registerTool("drd_list_personas", {
|
|
157
197
|
title: "List personas",
|
|
158
198
|
description: "List all personas.",
|
|
159
|
-
inputSchema: z.object(
|
|
160
|
-
}, async () => {
|
|
199
|
+
inputSchema: z.object(ws)
|
|
200
|
+
}, async (args) => {
|
|
201
|
+
assertPin(args, "drd_list_personas");
|
|
161
202
|
const data = await drdFetch("/api/personas");
|
|
162
203
|
return textContent(JSON.stringify(data.personas, null, 2));
|
|
163
204
|
});
|
|
164
205
|
server.registerTool("drd_list_accounts", {
|
|
165
206
|
title: "List accounts",
|
|
166
207
|
description: "List all accounts.",
|
|
167
|
-
inputSchema: z.object(
|
|
168
|
-
}, async () => {
|
|
208
|
+
inputSchema: z.object(ws)
|
|
209
|
+
}, async (args) => {
|
|
210
|
+
assertPin(args, "drd_list_accounts");
|
|
169
211
|
const data = await drdFetch("/api/accounts");
|
|
170
212
|
return textContent(JSON.stringify(data.accounts, null, 2));
|
|
171
213
|
});
|
|
172
214
|
server.registerTool("drd_list_partners", {
|
|
173
215
|
title: "List partners",
|
|
174
216
|
description: "List all partners.",
|
|
175
|
-
inputSchema: z.object(
|
|
176
|
-
}, async () => {
|
|
217
|
+
inputSchema: z.object(ws)
|
|
218
|
+
}, async (args) => {
|
|
219
|
+
assertPin(args, "drd_list_partners");
|
|
177
220
|
const data = await drdFetch("/api/partners");
|
|
178
221
|
return textContent(JSON.stringify(data.partners, null, 2));
|
|
179
222
|
});
|
|
180
|
-
// --- KPIs, milestones, stakeholders
|
|
181
223
|
server.registerTool("drd_list_kpis", {
|
|
182
224
|
title: "List KPIs",
|
|
183
225
|
description: "List all initiative KPIs with their initiative context (title, domain, owner).",
|
|
184
|
-
inputSchema: z.object(
|
|
185
|
-
}, async () => {
|
|
226
|
+
inputSchema: z.object(ws)
|
|
227
|
+
}, async (args) => {
|
|
228
|
+
assertPin(args, "drd_list_kpis");
|
|
186
229
|
const data = await drdFetch("/api/kpis");
|
|
187
230
|
return textContent(JSON.stringify(data.kpis, null, 2));
|
|
188
231
|
});
|
|
189
232
|
server.registerTool("drd_list_milestones", {
|
|
190
233
|
title: "List milestones",
|
|
191
234
|
description: "List all initiative milestones with their initiative context.",
|
|
192
|
-
inputSchema: z.object(
|
|
193
|
-
}, async () => {
|
|
235
|
+
inputSchema: z.object(ws)
|
|
236
|
+
}, async (args) => {
|
|
237
|
+
assertPin(args, "drd_list_milestones");
|
|
194
238
|
const data = await drdFetch("/api/milestones");
|
|
195
239
|
return textContent(JSON.stringify(data.milestones, null, 2));
|
|
196
240
|
});
|
|
197
241
|
server.registerTool("drd_list_demands", {
|
|
198
242
|
title: "List demands",
|
|
199
243
|
description: "List all demands (from accounts, partners, internal, compliance).",
|
|
200
|
-
inputSchema: z.object(
|
|
201
|
-
}, async () => {
|
|
244
|
+
inputSchema: z.object(ws)
|
|
245
|
+
}, async (args) => {
|
|
246
|
+
assertPin(args, "drd_list_demands");
|
|
202
247
|
const data = await drdFetch("/api/demands");
|
|
203
248
|
return textContent(JSON.stringify(data.demands, null, 2));
|
|
204
249
|
});
|
|
205
250
|
server.registerTool("drd_list_revenue_streams", {
|
|
206
251
|
title: "List revenue streams",
|
|
207
252
|
description: "List all revenue streams.",
|
|
208
|
-
inputSchema: z.object(
|
|
209
|
-
}, async () => {
|
|
253
|
+
inputSchema: z.object(ws)
|
|
254
|
+
}, async (args) => {
|
|
255
|
+
assertPin(args, "drd_list_revenue_streams");
|
|
210
256
|
const data = await drdFetch("/api/revenue-streams");
|
|
211
257
|
return textContent(JSON.stringify(data.revenueStreams, null, 2));
|
|
212
258
|
});
|
|
213
259
|
server.registerTool("tymio_get_coding_agent_guide", {
|
|
214
260
|
title: "Get Tymio coding agent playbook (Markdown)",
|
|
215
261
|
description: "Full docs/CODING_AGENT_TYMIO.md: MCP usage, as-is to Tymio, feature lifecycle. Call at session start when automating this hub.",
|
|
216
|
-
inputSchema: z.object(
|
|
217
|
-
}, async () => {
|
|
262
|
+
inputSchema: z.object(ws)
|
|
263
|
+
}, async (args) => {
|
|
264
|
+
assertPin(args, "tymio_get_coding_agent_guide");
|
|
218
265
|
const md = await drdFetchText("/api/agent/coding-guide");
|
|
219
266
|
return textContent(md);
|
|
220
267
|
});
|
|
221
268
|
server.registerTool("tymio_get_agent_brief", {
|
|
222
269
|
title: "Get compiled agent capability brief",
|
|
223
270
|
description: "Returns the hub capability ontology as Markdown or JSON. mode=compact|full, format=md|json.",
|
|
224
|
-
inputSchema: z
|
|
271
|
+
inputSchema: z
|
|
272
|
+
.object({
|
|
225
273
|
mode: z.enum(["compact", "full"]).default("compact"),
|
|
226
274
|
format: z.enum(["md", "json"]).default("md")
|
|
227
275
|
})
|
|
276
|
+
.extend(ws)
|
|
228
277
|
}, async (args) => {
|
|
229
|
-
|
|
278
|
+
assertPin(args, "tymio_get_agent_brief");
|
|
279
|
+
const { mode, format } = omitWorkspaceSlug(args);
|
|
280
|
+
const params = new URLSearchParams({ mode, format });
|
|
230
281
|
const q = params.toString();
|
|
231
|
-
if (args.format === "md") {
|
|
232
|
-
const text = await drdFetchText(`/api/ontology/brief?${q}`);
|
|
233
|
-
return textContent(text);
|
|
234
|
-
}
|
|
235
282
|
const raw = await drdFetchText(`/api/ontology/brief?${q}`);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
283
|
+
if (format === "json") {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
return textContent(JSON.stringify(parsed, null, 2));
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return textContent(raw);
|
|
290
|
+
}
|
|
242
291
|
}
|
|
292
|
+
return textContent(raw);
|
|
243
293
|
});
|
|
244
294
|
server.registerTool("tymio_list_capabilities", {
|
|
245
295
|
title: "List hub capabilities (ontology)",
|
|
246
296
|
description: "Optional status: ACTIVE, DRAFT, DEPRECATED.",
|
|
247
|
-
inputSchema: z.object({ status: z.enum(["ACTIVE", "DRAFT", "DEPRECATED"]).optional() })
|
|
297
|
+
inputSchema: z.object({ status: z.enum(["ACTIVE", "DRAFT", "DEPRECATED"]).optional() }).extend(ws)
|
|
248
298
|
}, async (args) => {
|
|
299
|
+
assertPin(args, "tymio_list_capabilities");
|
|
300
|
+
const { status } = omitWorkspaceSlug(args);
|
|
249
301
|
const params = new URLSearchParams();
|
|
250
|
-
if (
|
|
251
|
-
params.set("status",
|
|
302
|
+
if (status)
|
|
303
|
+
params.set("status", status);
|
|
252
304
|
const q = params.toString();
|
|
253
305
|
const data = await drdFetch(`/api/ontology/capabilities${q ? `?${q}` : ""}`);
|
|
254
306
|
return textContent(JSON.stringify(data, null, 2));
|
|
@@ -256,14 +308,18 @@ export async function runApiKeyStdio() {
|
|
|
256
308
|
server.registerTool("tymio_get_capability", {
|
|
257
309
|
title: "Get one capability by id or slug",
|
|
258
310
|
description: "Provide id or slug.",
|
|
259
|
-
inputSchema: z
|
|
311
|
+
inputSchema: z
|
|
312
|
+
.object({ id: z.string().optional(), slug: z.string().optional() })
|
|
313
|
+
.extend(ws)
|
|
260
314
|
}, async (args) => {
|
|
261
|
-
|
|
262
|
-
|
|
315
|
+
assertPin(args, "tymio_get_capability");
|
|
316
|
+
const { id, slug } = omitWorkspaceSlug(args);
|
|
317
|
+
if (id) {
|
|
318
|
+
const data = await drdFetch(`/api/ontology/capabilities/${id}`);
|
|
263
319
|
return textContent(JSON.stringify(data, null, 2));
|
|
264
320
|
}
|
|
265
|
-
if (
|
|
266
|
-
const data = await drdFetch(`/api/ontology/capabilities/by-slug/${encodeURIComponent(
|
|
321
|
+
if (slug) {
|
|
322
|
+
const data = await drdFetch(`/api/ontology/capabilities/by-slug/${encodeURIComponent(slug)}`);
|
|
267
323
|
return textContent(JSON.stringify(data, null, 2));
|
|
268
324
|
}
|
|
269
325
|
throw new Error("Provide id or slug");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { drdFetch } from "./api.js";
|
|
2
|
+
import { isValidWorkspaceSlugFormat } from "./workspaceSlug.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve workspace slug to tenant id for API-key bridge; verifies ACTIVE membership for the API key user.
|
|
5
|
+
*/
|
|
6
|
+
export async function resolveTenantIdForWorkspaceSlug(expectedSlug) {
|
|
7
|
+
if (!isValidWorkspaceSlugFormat(expectedSlug)) {
|
|
8
|
+
throw new Error(`Invalid workspace slug: ${JSON.stringify(expectedSlug)}`);
|
|
9
|
+
}
|
|
10
|
+
const want = expectedSlug.toLowerCase();
|
|
11
|
+
const data = await drdFetch("/api/me/tenants");
|
|
12
|
+
const row = data.tenants.find((m) => m.tenant.slug.toLowerCase() === want && m.tenant.status === "ACTIVE");
|
|
13
|
+
if (!row) {
|
|
14
|
+
throw new Error(`API key user has no ACTIVE membership for workspace slug "${expectedSlug}". Check TYMIO_WORKSPACE_SLUG and hub memberships.`);
|
|
15
|
+
}
|
|
16
|
+
return row.tenant.id;
|
|
17
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runApiKeyStdio } from "./apiKeyStdio.js";
|
|
|
4
4
|
import { runHubOAuthStdio } from "./hubProxyStdio.js";
|
|
5
5
|
import { runLoginCommand } from "./loginCommand.js";
|
|
6
6
|
import { removeAllOAuthFiles } from "./fileOAuthProvider.js";
|
|
7
|
+
import { runPersonaCli } from "./persona.js";
|
|
7
8
|
function useApiKeyBridge() {
|
|
8
9
|
return Boolean(process.env.DRD_API_KEY?.trim() || process.env.API_KEY?.trim());
|
|
9
10
|
}
|
|
@@ -27,6 +28,10 @@ export async function runCli(argv) {
|
|
|
27
28
|
process.stderr.write(`${HELP_SUMMARY}\n`);
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
31
|
+
if (args[0] === "persona") {
|
|
32
|
+
process.exitCode = runPersonaCli(args.slice(1));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
if (useApiKeyBridge()) {
|
|
31
36
|
await runApiKeyStdio();
|
|
32
37
|
return;
|
package/dist/cliMessages.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Short usage (stderr) — keep in sync with guidance file for agents. */
|
|
2
|
-
export declare const HELP_SUMMARY = "Tymio MCP CLI (@tymio/mcp-server)\n\nCommands:\n tymio-mcp Start stdio MCP (default: OAuth \u2192 hosted Tymio MCP)\n tymio-mcp login [url] Sign in with Google (browser). Saves tokens locally.\n tymio-mcp logout Delete saved OAuth client + tokens\n tymio-mcp instructions Full setup text for humans & coding agents (print this)\n tymio-mcp help This summary\n\nEnvironment:\n TYMIO_MCP_URL Hosted MCP URL (default https://tymio.app/mcp)\n TYMIO_OAUTH_PORT Loopback port for login callback (default 19876)\n TYMIO_MCP_QUIET If set, suppress stderr hints when starting stdio\n DRD_API_KEY / API_KEY If set \u2192 API-key REST tool bridge (subset), not OAuth proxy\n DRD_API_BASE_URL Hub origin for API-key bridge (default https://tymio.app)\n\nCritical for agents: There is NO MCP API key in Tymio user Settings \u2014 use OAuth (remote /mcp URL or tymio-mcp login).\nTip: Run tymio-mcp instructions for the full Markdown guide, Cursor JSON, and troubleshooting.\n";
|
|
2
|
+
export declare const HELP_SUMMARY = "Tymio MCP CLI (@tymio/mcp-server)\n\nCommands:\n tymio-mcp Start stdio MCP (default: OAuth \u2192 hosted Tymio MCP)\n tymio-mcp login [url] Sign in with Google (browser). Saves tokens locally.\n tymio-mcp logout Delete saved OAuth client + tokens\n tymio-mcp instructions Full setup text for humans & coding agents (print this)\n tymio-mcp persona list Bundled PM/PO/DEV/workspace prompts (see also TYMIO_MCP_PERSONA)\n tymio-mcp persona <id> Print one persona Markdown to stdout (pm | po | dev | workspace)\n tymio-mcp help This summary\n\nEnvironment:\n TYMIO_MCP_URL Hosted MCP URL (default https://tymio.app/mcp)\n TYMIO_OAUTH_PORT Loopback port for login callback (default 19876)\n TYMIO_MCP_QUIET If set, suppress stderr hints when starting stdio\n TYMIO_WORKSPACE_SLUG Required for stdio (or DRD_WORKSPACE_SLUG): hub workspace slug this process is pinned to; every tool call must use the same slug (tests: TYMIO_MCP_SKIP_WORKSPACE_PINNING=1)\n TYMIO_MCP_PERSONA Optional: pm | po | dev | workspace (hub aliases workspace) \u2014 appended to MCP server instructions\n DRD_API_KEY / API_KEY If set \u2192 API-key REST tool bridge (subset), not OAuth proxy\n DRD_API_BASE_URL Hub origin for API-key bridge (default https://tymio.app)\n\nCritical for agents: There is NO MCP API key in Tymio user Settings \u2014 use OAuth (remote /mcp URL or tymio-mcp login).\nTip: Run tymio-mcp instructions for the full Markdown guide, Cursor JSON, and troubleshooting.\n";
|
|
3
3
|
/**
|
|
4
4
|
* Long-form instructions for coding agents and operators (Markdown).
|
|
5
5
|
* Loaded from `TYMIO_MCP_CLI_AGENT_GUIDANCE.md` beside the installed package / dist output.
|
package/dist/cliMessages.js
CHANGED
|
@@ -34,12 +34,16 @@ Commands:
|
|
|
34
34
|
tymio-mcp login [url] Sign in with Google (browser). Saves tokens locally.
|
|
35
35
|
tymio-mcp logout Delete saved OAuth client + tokens
|
|
36
36
|
tymio-mcp instructions Full setup text for humans & coding agents (print this)
|
|
37
|
+
tymio-mcp persona list Bundled PM/PO/DEV/workspace prompts (see also TYMIO_MCP_PERSONA)
|
|
38
|
+
tymio-mcp persona <id> Print one persona Markdown to stdout (pm | po | dev | workspace)
|
|
37
39
|
tymio-mcp help This summary
|
|
38
40
|
|
|
39
41
|
Environment:
|
|
40
42
|
TYMIO_MCP_URL Hosted MCP URL (default https://tymio.app/mcp)
|
|
41
43
|
TYMIO_OAUTH_PORT Loopback port for login callback (default 19876)
|
|
42
44
|
TYMIO_MCP_QUIET If set, suppress stderr hints when starting stdio
|
|
45
|
+
TYMIO_WORKSPACE_SLUG Required for stdio (or DRD_WORKSPACE_SLUG): hub workspace slug this process is pinned to; every tool call must use the same slug (tests: TYMIO_MCP_SKIP_WORKSPACE_PINNING=1)
|
|
46
|
+
TYMIO_MCP_PERSONA Optional: pm | po | dev | workspace (hub aliases workspace) — appended to MCP server instructions
|
|
43
47
|
DRD_API_KEY / API_KEY If set → API-key REST tool bridge (subset), not OAuth proxy
|
|
44
48
|
DRD_API_BASE_URL Hub origin for API-key bridge (default https://tymio.app)
|
|
45
49
|
|
package/dist/hubProxyStdio.js
CHANGED
|
@@ -7,8 +7,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { defaultMcpUrl, defaultOAuthRedirectUrl } from "./configPaths.js";
|
|
9
9
|
import { FileOAuthProvider } from "./fileOAuthProvider.js";
|
|
10
|
-
import {
|
|
10
|
+
import { getMcpServerInstructions } from "./persona.js";
|
|
11
11
|
import { writeStdioStartupHint } from "./stdioHints.js";
|
|
12
|
+
import { assertToolArgsMatchPinnedWorkspace, readPinnedWorkspaceSlugForStdio } from "./workspaceSlug.js";
|
|
12
13
|
function pkgVersion() {
|
|
13
14
|
try {
|
|
14
15
|
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
@@ -24,6 +25,7 @@ const passthroughArgs = z.object({}).passthrough();
|
|
|
24
25
|
*/
|
|
25
26
|
export async function runHubOAuthStdio(mcpUrl = defaultMcpUrl()) {
|
|
26
27
|
writeStdioStartupHint("oauth");
|
|
28
|
+
const pinnedWorkspaceSlug = readPinnedWorkspaceSlugForStdio();
|
|
27
29
|
const redirectUrl = defaultOAuthRedirectUrl();
|
|
28
30
|
const provider = new FileOAuthProvider(redirectUrl);
|
|
29
31
|
const transport = new StreamableHTTPClientTransport(mcpUrl, { authProvider: provider });
|
|
@@ -39,7 +41,7 @@ export async function runHubOAuthStdio(mcpUrl = defaultMcpUrl()) {
|
|
|
39
41
|
throw e;
|
|
40
42
|
}
|
|
41
43
|
const { tools } = await client.listTools();
|
|
42
|
-
const server = new McpServer({ name: "tymio-hub", version: pkgVersion() }, { instructions:
|
|
44
|
+
const server = new McpServer({ name: "tymio-hub", version: pkgVersion() }, { instructions: getMcpServerInstructions() });
|
|
43
45
|
for (const tool of tools) {
|
|
44
46
|
const name = tool.name;
|
|
45
47
|
server.registerTool(name, {
|
|
@@ -47,6 +49,9 @@ export async function runHubOAuthStdio(mcpUrl = defaultMcpUrl()) {
|
|
|
47
49
|
description: tool.description ?? "",
|
|
48
50
|
inputSchema: passthroughArgs
|
|
49
51
|
}, async (args) => {
|
|
52
|
+
if (pinnedWorkspaceSlug) {
|
|
53
|
+
assertToolArgsMatchPinnedWorkspace(args ?? {}, pinnedWorkspaceSlug, name);
|
|
54
|
+
}
|
|
50
55
|
const result = await client.callTool({
|
|
51
56
|
name,
|
|
52
57
|
arguments: args ?? {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const PERSONA_IDS: readonly ["pm", "po", "dev", "workspace"];
|
|
2
|
+
export type PersonaId = (typeof PERSONA_IDS)[number];
|
|
3
|
+
/** `hub` is an alias for `workspace` (base hub agent behavior). */
|
|
4
|
+
export declare function normalizePersonaId(raw: string | undefined): PersonaId | null;
|
|
5
|
+
export declare function listPersonaIds(): readonly PersonaId[];
|
|
6
|
+
export declare function loadPersonaMarkdown(id: PersonaId): string;
|
|
7
|
+
export declare function personaListHelpText(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Full MCP server `instructions`: base CLI guide plus optional persona from `TYMIO_MCP_PERSONA`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getMcpServerInstructions(): string;
|
|
12
|
+
/** Non-empty when a valid persona is active (for stderr startup hint). */
|
|
13
|
+
export declare function activePersonaForHint(): PersonaId | null;
|
|
14
|
+
/** CLI subcommand: `tymio-mcp persona …` — exit code. */
|
|
15
|
+
export declare function runPersonaCli(argv: string[]): number;
|
|
16
|
+
export {};
|
package/dist/persona.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { AGENT_INSTRUCTIONS } from "./cliMessages.js";
|
|
5
|
+
const PERSONA_IDS = ["pm", "po", "dev", "workspace"];
|
|
6
|
+
function personasDir() {
|
|
7
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "personas");
|
|
8
|
+
}
|
|
9
|
+
/** `hub` is an alias for `workspace` (base hub agent behavior). */
|
|
10
|
+
export function normalizePersonaId(raw) {
|
|
11
|
+
if (!raw?.trim())
|
|
12
|
+
return null;
|
|
13
|
+
const s = raw.trim().toLowerCase();
|
|
14
|
+
if (s === "hub")
|
|
15
|
+
return "workspace";
|
|
16
|
+
if (PERSONA_IDS.includes(s))
|
|
17
|
+
return s;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function listPersonaIds() {
|
|
21
|
+
return PERSONA_IDS;
|
|
22
|
+
}
|
|
23
|
+
export function loadPersonaMarkdown(id) {
|
|
24
|
+
const file = path.join(personasDir(), `${id}.md`);
|
|
25
|
+
if (!existsSync(file)) {
|
|
26
|
+
throw new Error(`Bundled persona file missing: ${file}`);
|
|
27
|
+
}
|
|
28
|
+
return readFileSync(file, "utf8");
|
|
29
|
+
}
|
|
30
|
+
export function personaListHelpText() {
|
|
31
|
+
return `Tymio MCP — bundled agent personas (Markdown prompts)
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
tymio-mcp persona list Show this list
|
|
35
|
+
tymio-mcp persona <id> Print persona prompt to stdout (pipe into your agent / docs)
|
|
36
|
+
|
|
37
|
+
Ids: ${PERSONA_IDS.join(", ")}
|
|
38
|
+
|
|
39
|
+
Embed in MCP sessions (stdio / some clients read server instructions):
|
|
40
|
+
export TYMIO_MCP_PERSONA=pm # or po, dev, workspace
|
|
41
|
+
tymio-mcp # persona text is appended to MCP server instructions
|
|
42
|
+
|
|
43
|
+
Cursor: you can instead enable Skills (.cursor/skills/tymio-*-agent); CLI personas help IDEs without Skills or CI.
|
|
44
|
+
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Full MCP server `instructions`: base CLI guide plus optional persona from `TYMIO_MCP_PERSONA`.
|
|
49
|
+
*/
|
|
50
|
+
export function getMcpServerInstructions() {
|
|
51
|
+
const raw = process.env.TYMIO_MCP_PERSONA;
|
|
52
|
+
if (!raw?.trim())
|
|
53
|
+
return AGENT_INSTRUCTIONS;
|
|
54
|
+
const id = normalizePersonaId(raw);
|
|
55
|
+
if (!id) {
|
|
56
|
+
process.stderr.write(`[tymio-mcp] Unknown TYMIO_MCP_PERSONA="${raw.trim()}". Valid: ${PERSONA_IDS.join(", ")} (hub aliases workspace). See: tymio-mcp persona list\n`);
|
|
57
|
+
return AGENT_INSTRUCTIONS;
|
|
58
|
+
}
|
|
59
|
+
let block;
|
|
60
|
+
try {
|
|
61
|
+
block = loadPersonaMarkdown(id);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
process.stderr.write(`[tymio-mcp] Could not load persona "${id}": ${e}\n`);
|
|
65
|
+
return AGENT_INSTRUCTIONS;
|
|
66
|
+
}
|
|
67
|
+
return `${AGENT_INSTRUCTIONS}\n\n---\n\n## Bundled agent persona (\`TYMIO_MCP_PERSONA=${id}\`)\n\n${block}\n`;
|
|
68
|
+
}
|
|
69
|
+
/** Non-empty when a valid persona is active (for stderr startup hint). */
|
|
70
|
+
export function activePersonaForHint() {
|
|
71
|
+
return normalizePersonaId(process.env.TYMIO_MCP_PERSONA);
|
|
72
|
+
}
|
|
73
|
+
/** CLI subcommand: `tymio-mcp persona …` — exit code. */
|
|
74
|
+
export function runPersonaCli(argv) {
|
|
75
|
+
const sub = argv[0];
|
|
76
|
+
if (!sub || sub === "list" || sub === "--help" || sub === "-h") {
|
|
77
|
+
process.stderr.write(personaListHelpText());
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const id = normalizePersonaId(sub);
|
|
81
|
+
if (!id) {
|
|
82
|
+
process.stderr.write(`Unknown persona "${sub}". Run: tymio-mcp persona list\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
process.stdout.write(loadPersonaMarkdown(id));
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
process.stderr.write(String(e) + "\n");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/stdioHints.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { activePersonaForHint } from "./persona.js";
|
|
1
2
|
/**
|
|
2
3
|
* One-line stderr hint when starting stdio (does not touch stdout — MCP JSON-RPC stays clean).
|
|
3
4
|
* Suppress with TYMIO_MCP_QUIET=1 or non-TTY stderr.
|
|
@@ -8,9 +9,13 @@ export function writeStdioStartupHint(mode) {
|
|
|
8
9
|
if (!process.stderr.isTTY)
|
|
9
10
|
return;
|
|
10
11
|
if (mode === "oauth") {
|
|
11
|
-
process.stderr.write("[tymio-mcp] OAuth proxy to Tymio MCP. No MCP key in Tymio Settings — use login/OAuth. First run: `tymio-mcp login`. Guide: `tymio-mcp instructions` | `tymio-mcp help`\n");
|
|
12
|
+
process.stderr.write("[tymio-mcp] OAuth proxy to Tymio MCP. No MCP key in Tymio Settings — use login/OAuth. First run: `tymio-mcp login`. Set TYMIO_WORKSPACE_SLUG (or DRD_WORKSPACE_SLUG) to pin this server to one workspace. Guide: `tymio-mcp instructions` | `tymio-mcp help`\n");
|
|
12
13
|
}
|
|
13
14
|
else {
|
|
14
|
-
process.stderr.write("[tymio-mcp] API-key REST bridge. Set DRD_API_BASE_URL + DRD_API_KEY. Agent guide: `tymio-mcp instructions`\n");
|
|
15
|
+
process.stderr.write("[tymio-mcp] API-key REST bridge. Set DRD_API_BASE_URL + DRD_API_KEY + TYMIO_WORKSPACE_SLUG (tenant resolved to X-Tenant-Id). Agent guide: `tymio-mcp instructions`\n");
|
|
16
|
+
}
|
|
17
|
+
const persona = activePersonaForHint();
|
|
18
|
+
if (persona) {
|
|
19
|
+
process.stderr.write(`[tymio-mcp] TYMIO_MCP_PERSONA=${persona} — persona text is appended to MCP server instructions. Print prompt: tymio-mcp persona ${persona}\n`);
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Matches hub workspace slug rules (see server tenant slug validation). */
|
|
3
|
+
export declare const WORKSPACE_SLUG_ZOD: z.ZodString;
|
|
4
|
+
export declare function isValidWorkspaceSlugFormat(slug: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Pinned slug for this stdio process: every proxied MCP tool call must use this workspace.
|
|
7
|
+
* Set `TYMIO_MCP_SKIP_WORKSPACE_PINNING=1` only in tests.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readPinnedWorkspaceSlugForStdio(): string | null;
|
|
10
|
+
/** Enforce agent-supplied slug matches pinned CLI config (defense in depth vs hub session). */
|
|
11
|
+
export declare function assertToolArgsMatchPinnedWorkspace(args: unknown, pinnedSlug: string, toolName: string): void;
|
|
12
|
+
/** After assert, remove workspaceSlug before REST bodies. */
|
|
13
|
+
export declare function omitWorkspaceSlug<T extends Record<string, unknown>>(args: T): Omit<T, "workspaceSlug">;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Matches hub workspace slug rules (see server tenant slug validation). */
|
|
3
|
+
export const WORKSPACE_SLUG_ZOD = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(2)
|
|
6
|
+
.max(50)
|
|
7
|
+
.regex(/^[a-z0-9-]+$/, "Workspace slug: 2–50 chars, lowercase a-z, digits, hyphens only.");
|
|
8
|
+
export function isValidWorkspaceSlugFormat(slug) {
|
|
9
|
+
return WORKSPACE_SLUG_ZOD.safeParse(slug).success;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pinned slug for this stdio process: every proxied MCP tool call must use this workspace.
|
|
13
|
+
* Set `TYMIO_MCP_SKIP_WORKSPACE_PINNING=1` only in tests.
|
|
14
|
+
*/
|
|
15
|
+
export function readPinnedWorkspaceSlugForStdio() {
|
|
16
|
+
if (process.env.TYMIO_MCP_SKIP_WORKSPACE_PINNING === "1") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const raw = process.env.TYMIO_WORKSPACE_SLUG?.trim() || process.env.DRD_WORKSPACE_SLUG?.trim();
|
|
20
|
+
if (!raw) {
|
|
21
|
+
process.stderr.write("[tymio-mcp] Missing TYMIO_WORKSPACE_SLUG or DRD_WORKSPACE_SLUG. Set this to your hub workspace slug (e.g. acme-corp). Required so this MCP server only operates on one workspace; tool args must match.\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const parsed = WORKSPACE_SLUG_ZOD.safeParse(raw);
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
process.stderr.write(`[tymio-mcp] Invalid workspace slug: ${JSON.stringify(raw)}. Use 2–50 characters: lowercase letters, digits, hyphens only.\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return parsed.data;
|
|
30
|
+
}
|
|
31
|
+
/** Enforce agent-supplied slug matches pinned CLI config (defense in depth vs hub session). */
|
|
32
|
+
export function assertToolArgsMatchPinnedWorkspace(args, pinnedSlug, toolName) {
|
|
33
|
+
if (!args || typeof args !== "object") {
|
|
34
|
+
throw new Error(`[tymio-mcp] ${toolName}: missing or invalid arguments object.`);
|
|
35
|
+
}
|
|
36
|
+
const o = args;
|
|
37
|
+
const slug = o.workspaceSlug;
|
|
38
|
+
if (typeof slug !== "string") {
|
|
39
|
+
throw new Error(`[tymio-mcp] ${toolName}: workspaceSlug is required on every tool call (string, must match ${pinnedSlug}).`);
|
|
40
|
+
}
|
|
41
|
+
const t = slug.trim().toLowerCase();
|
|
42
|
+
if (!isValidWorkspaceSlugFormat(t)) {
|
|
43
|
+
throw new Error(`[tymio-mcp] ${toolName}: invalid workspaceSlug format. Use 2–50 chars: lowercase a-z, digits, hyphens.`);
|
|
44
|
+
}
|
|
45
|
+
if (t !== pinnedSlug.toLowerCase()) {
|
|
46
|
+
throw new Error(`[tymio-mcp] ${toolName}: workspaceSlug "${slug}" does not match this MCP server pin "${pinnedSlug}". Refusing cross-workspace access.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** After assert, remove workspaceSlug before REST bodies. */
|
|
50
|
+
export function omitWorkspaceSlug(args) {
|
|
51
|
+
const { workspaceSlug: _, ...rest } = args;
|
|
52
|
+
return rest;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tymio/mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Tymio MCP CLI: OAuth stdio proxy to hosted MCP,
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Tymio MCP CLI: OAuth stdio proxy to hosted MCP, API-key REST bridge, bundled PM/PO/DEV/workspace agent personas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"personas",
|
|
13
|
+
"CHANGELOG.md",
|
|
12
14
|
"README.md",
|
|
13
15
|
"TYMIO_MCP_CLI_AGENT_GUIDANCE.md"
|
|
14
16
|
],
|
package/personas/dev.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Tymio — Developer agent
|
|
2
|
+
|
|
3
|
+
You act as a **developer** whose scope is defined in **Tymio**. **Read** the hub for what to build; **implement** in the user’s repo. You do **not** own roadmap/backlog unless explicitly asked to update hub rows.
|
|
4
|
+
|
|
5
|
+
## Ontology
|
|
6
|
+
|
|
7
|
+
Default **leaf** is **Requirement** → parent **Feature** → **Initiative**. Use **`tymio_get_agent_brief`** / **`tymio_get_coding_agent_guide`** for product/API truth; use backlog tools for scope. **Dependencies** between bets are initiative-level. Monorepo reference: `.cursor/skills/tymio-workspace/references/tymio-hub-ontology.md`.
|
|
8
|
+
|
|
9
|
+
## Before you code
|
|
10
|
+
|
|
11
|
+
1. **`tymio_get_agent_brief`**; heavy implementation: **`tymio_get_coding_agent_guide`**.
|
|
12
|
+
2. Map work to initiative/feature/requirement IDs via **`drd_list_*`** / **`drd_get_initiative`**.
|
|
13
|
+
3. If hub reads fail, report it; fix MCP/OAuth.
|
|
14
|
+
|
|
15
|
+
## Data
|
|
16
|
+
|
|
17
|
+
| Need | Tools |
|
|
18
|
+
|------|--------|
|
|
19
|
+
| Acceptance | **`drd_list_requirements`** (update only if user asked) |
|
|
20
|
+
| Packaging | **`drd_list_features`**, **`drd_list_initiatives`** |
|
|
21
|
+
| Blockers | **`drd_list_dependencies`**, decisions, risks |
|
|
22
|
+
| Surfaces | **`tymio_list_capabilities`**, **`tymio_get_capability`** |
|
|
23
|
+
| Taxonomy | **`drd_meta`** |
|
|
24
|
+
|
|
25
|
+
## Avoid
|
|
26
|
+
|
|
27
|
+
- Reprioritizing initiatives (PM).
|
|
28
|
+
- Bulk-creating features/requirements without PO-style instruction.
|
|
29
|
+
- Treating the coding guide as permission to change deployment secrets or admin settings.
|
|
30
|
+
|
|
31
|
+
## Output
|
|
32
|
+
|
|
33
|
+
Open with requirement/feature IDs/titles; PR summaries link hub records to files; ask when requirements are ambiguous before large refactors.
|
package/personas/pm.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Tymio — Product Manager agent
|
|
2
|
+
|
|
3
|
+
You act as a **Product Manager** on the **Tymio hub**. Focus **portfolio and roadmap coherence** (themes, bets, tradeoffs, stakeholders, signals) — not fine-grained backlog grooming (defer to the PO persona).
|
|
4
|
+
|
|
5
|
+
## Ontology
|
|
6
|
+
|
|
7
|
+
Internalize the **backlog graph** before reasoning: Initiatives under Domains; Features under Initiatives; Requirements under Features; **Dependency** links initiatives, not features. Separate that from the **capability** brief (`tymio_*`). Monorepo reference: `.cursor/skills/tymio-workspace/references/tymio-hub-ontology.md`.
|
|
8
|
+
|
|
9
|
+
## Before you reason
|
|
10
|
+
|
|
11
|
+
1. **`tymio_get_agent_brief`** and **`drd_meta`** — do not invent domain/product/tool names.
|
|
12
|
+
2. If MCP fails, fix OAuth / `tymio-mcp login`; never tell users to copy an MCP key from Tymio Settings.
|
|
13
|
+
3. Assume **`VIEWER`/`EDITOR`** unless known otherwise.
|
|
14
|
+
|
|
15
|
+
## Workflows
|
|
16
|
+
|
|
17
|
+
1. **`drd_meta`**, **`drd_list_domains`**, **`drd_list_products`**, optional **`drd_get_product_tree`**.
|
|
18
|
+
2. **`drd_list_initiatives`** (filters as supported).
|
|
19
|
+
3. **`drd_get_initiative`** → **`drd_list_decisions`**, **`drd_list_risks`**, **`drd_list_stakeholders`**, timeline tools.
|
|
20
|
+
4. Signals: **`drd_list_demands`**, **`drd_list_accounts`**, **`drd_list_partners`**, KPIs/milestones/personas as needed.
|
|
21
|
+
5. Mutations only if permitted: **`drd_create_initiative`** / **`drd_update_initiative`** — no bulk delete without explicit confirmation.
|
|
22
|
+
|
|
23
|
+
## Avoid
|
|
24
|
+
|
|
25
|
+
- Defaulting to rewriting requirements/features (PO/Dev).
|
|
26
|
+
- Deep coding-guide dives unless the user asks (use Dev persona).
|
|
27
|
+
- Guessing IDs — resolve via meta/list tools.
|
|
28
|
+
|
|
29
|
+
## Output
|
|
30
|
+
|
|
31
|
+
Hub facts first, then recommendations; separate “in hub” vs “proposed”; stakeholder summaries tie to domains/products, KPIs/milestones, decisions/risks.
|
package/personas/po.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Tymio — Product Owner agent
|
|
2
|
+
|
|
3
|
+
You act as a **Product Owner** on the **Tymio hub**. Focus **backlog refinement and delivery readiness**: features, requirements, acceptance, ownership, dependencies, timeline — not portfolio strategy (PM persona).
|
|
4
|
+
|
|
5
|
+
## Ontology
|
|
6
|
+
|
|
7
|
+
Follow **Domain/Product → Initiative → Feature → Requirement** before creating rows. Never create a **Feature** without a real `initiativeId` or a **Requirement** without a real `featureId`. **Dependency** in the hub is **initiative-level**. Monorepo reference: `.cursor/skills/tymio-workspace/references/tymio-hub-ontology.md`.
|
|
8
|
+
|
|
9
|
+
## Before you write
|
|
10
|
+
|
|
11
|
+
1. **`tymio_get_agent_brief`** then **`drd_meta`**.
|
|
12
|
+
2. Fix auth if tools fail; no MCP key in user Settings.
|
|
13
|
+
3. Creating/updating work typically needs **EDITOR+**.
|
|
14
|
+
|
|
15
|
+
## Workflows
|
|
16
|
+
|
|
17
|
+
1. **`drd_list_initiatives`** → **`drd_get_initiative`**.
|
|
18
|
+
2. **`drd_list_features`** → create/update features.
|
|
19
|
+
3. **`drd_list_requirements`** → create/update/upsert requirements with testable acceptance.
|
|
20
|
+
4. **`drd_list_assignments`**, **`drd_list_dependencies`**, decisions/risks for blockers.
|
|
21
|
+
5. Timeline tools for communication, not as a substitute for requirements.
|
|
22
|
+
|
|
23
|
+
## Handoffs
|
|
24
|
+
|
|
25
|
+
From PM: prioritized initiatives; to Dev: requirements/features should stand alone for implementation questions.
|
|
26
|
+
|
|
27
|
+
## Avoid
|
|
28
|
+
|
|
29
|
+
- Deletes without explicit user confirmation.
|
|
30
|
+
- Silent initiative priority/horizon changes when the user only asked for requirement edits.
|
|
31
|
+
- Invented dependency edges.
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
|
|
35
|
+
Current state (IDs + titles) → proposed edits → open questions; small verifiable requirements; status changes with from → to and why.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Tymio workspace (agents)
|
|
2
|
+
|
|
3
|
+
Bundled with `@tymio/mcp-server` for MCP `instructions` when `TYMIO_MCP_PERSONA=workspace` (default hub behavior is unchanged; this block is optional context).
|
|
4
|
+
|
|
5
|
+
When working **in the Tymio monorepo**, the API is often `http://localhost:8080` and MCP at `http://localhost:8080/mcp`.
|
|
6
|
+
|
|
7
|
+
## Before any mutation
|
|
8
|
+
|
|
9
|
+
1. Confirm MCP is connected; if tools are missing or auth fails, do not claim hub data changed.
|
|
10
|
+
2. Almost all `/api/*` needs a session or `Authorization: Bearer` deployment key where enabled.
|
|
11
|
+
3. Prefer **`tymio_get_agent_brief`** (or `GET /api/ontology/brief`) before assuming which routes or tools exist.
|
|
12
|
+
|
|
13
|
+
## Connect
|
|
14
|
+
|
|
15
|
+
- **Remote MCP:** `POST https://tymio.app/mcp` (or your host) with OAuth — no per-user MCP API key in Tymio Settings.
|
|
16
|
+
- **Stdio:** `tymio-mcp login` then run `tymio-mcp` without `DRD_API_KEY`/`API_KEY` for the full proxied tool list. With those env vars set, only a REST subset is available.
|
|
17
|
+
|
|
18
|
+
## Vocabulary
|
|
19
|
+
|
|
20
|
+
| In conversation | In Tymio |
|
|
21
|
+
|-----------------|----------|
|
|
22
|
+
| App / application (surface) | Usually **Product** (line / asset) |
|
|
23
|
+
| Tenant / customer org | **Workspace** |
|
|
24
|
+
| “Capability” in ontology | Product **affordance** (routes, tools, models) — **not** a backlog row |
|
|
25
|
+
|
|
26
|
+
**Flow:** demand/idea → **Initiative** → **Features** → **Requirements** (with domain/product from meta).
|
|
27
|
+
|
|
28
|
+
## Hub ontology (two layers)
|
|
29
|
+
|
|
30
|
+
1. **Backlog graph:** Domain → Initiative → Feature → Requirement; demands link to initiatives/features; **dependencies** are initiative→initiative in the default model.
|
|
31
|
+
2. **Capability brief:** `tymio_get_agent_brief`, `tymio_list_capabilities` — what the product exposes.
|
|
32
|
+
|
|
33
|
+
Use **`drd_meta`** then list/get tools for live tenant data. Full Mermaid + tables live in the monorepo: `.cursor/skills/tymio-workspace/references/tymio-hub-ontology.md`.
|
|
34
|
+
|
|
35
|
+
## Roles
|
|
36
|
+
|
|
37
|
+
`VIEWER`, `EDITOR`, `ADMIN`, `SUPER_ADMIN` — assume least privilege.
|
|
38
|
+
|
|
39
|
+
## Personas
|
|
40
|
+
|
|
41
|
+
PM / PO / DEV prompts: `tymio-mcp persona pm|po|dev` or set `TYMIO_MCP_PERSONA`. Role matrix: `docs/TYMIO_AGENT_ROLES_PM_PO_DEV.md` in the monorepo.
|