@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 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 = { "Content-Type": "application/json" };
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;
@@ -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 { drdFetch, drdFetchText, getBaseUrl, hasApiKey } from "./api.js";
8
- import { AGENT_INSTRUCTIONS } from "./cliMessages.js";
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 server = new McpServer({ name: "tymio-hub", version: "1.0.0" }, { instructions: AGENT_INSTRUCTIONS });
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
- // --- Health & meta (no auth required for health)
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
- // --- Initiatives
35
- const listInitiativesSchema = z.object({
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 (args.domainId)
49
- params.set("domainId", args.domainId);
50
- if (args.ownerId)
51
- params.set("ownerId", args.ownerId);
52
- if (args.horizon)
53
- params.set("horizon", args.horizon);
54
- if (args.priority)
55
- params.set("priority", args.priority);
56
- if (args.isGap !== undefined)
57
- params.set("isGap", String(args.isGap));
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 ({ id }) => {
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.object({
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
- }, async (body) => {
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.object({
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
- }, async ({ id, ...body }) => {
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 ({ id }) => {
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.object({
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
- }, async (body) => {
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.object({
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
- const params = new URLSearchParams({ mode: args.mode, format: args.format });
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
- try {
237
- const parsed = JSON.parse(raw);
238
- return textContent(JSON.stringify(parsed, null, 2));
239
- }
240
- catch {
241
- return textContent(raw);
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 (args.status)
251
- params.set("status", args.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.object({ id: z.string().optional(), slug: z.string().optional() })
311
+ inputSchema: z
312
+ .object({ id: z.string().optional(), slug: z.string().optional() })
313
+ .extend(ws)
260
314
  }, async (args) => {
261
- if (args.id) {
262
- const data = await drdFetch(`/api/ontology/capabilities/${args.id}`);
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 (args.slug) {
266
- const data = await drdFetch(`/api/ontology/capabilities/by-slug/${encodeURIComponent(args.slug)}`);
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,4 @@
1
+ /**
2
+ * Resolve workspace slug to tenant id for API-key bridge; verifies ACTIVE membership for the API key user.
3
+ */
4
+ export declare function resolveTenantIdForWorkspaceSlug(expectedSlug: string): Promise<string>;
@@ -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;
@@ -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.
@@ -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
 
@@ -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 { AGENT_INSTRUCTIONS } from "./cliMessages.js";
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: AGENT_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 {};
@@ -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
+ }
@@ -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": "1.0.1",
4
- "description": "Tymio MCP CLI: OAuth stdio proxy to hosted MCP, or API-key REST tool bridge",
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
  ],
@@ -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.