frinus-mcp 3.5.0 → 3.6.1

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.
@@ -17,4 +17,7 @@ export declare class CpClient implements CpClientInterface {
17
17
  getCredential(integrationRef: string): Promise<any>;
18
18
  listCredentials(): Promise<any>;
19
19
  deleteCredential(integrationRef: string): Promise<void>;
20
+ listCredentialShares(integrationRef: string): Promise<any>;
21
+ shareCredential(integrationRef: string, userIdOrEmail: string): Promise<any>;
22
+ revokeCredentialShare(integrationRef: string, granteeUserId: string): Promise<void>;
20
23
  }
@@ -57,4 +57,20 @@ export class CpClient {
57
57
  async deleteCredential(integrationRef) {
58
58
  await this.client.delete(`/api/v1/credentials/${integrationRef}`);
59
59
  }
60
+ async listCredentialShares(integrationRef) {
61
+ const response = await this.client.get(`/api/v1/credentials/${integrationRef}/shares`);
62
+ return response.data;
63
+ }
64
+ async shareCredential(integrationRef, userIdOrEmail) {
65
+ // The API accepts either field; pick by shape so callers can pass
66
+ // whichever they have. An email is what a human or agent usually knows.
67
+ const body = userIdOrEmail.includes("@")
68
+ ? { email: userIdOrEmail }
69
+ : { user_id: userIdOrEmail };
70
+ const response = await this.client.post(`/api/v1/credentials/${integrationRef}/shares`, body);
71
+ return response.data;
72
+ }
73
+ async revokeCredentialShare(integrationRef, granteeUserId) {
74
+ await this.client.delete(`/api/v1/credentials/${integrationRef}/shares/${granteeUserId}`);
75
+ }
60
76
  }
package/dist/index.js CHANGED
@@ -64,7 +64,7 @@ async function main() {
64
64
  }
65
65
  const server = new Server({
66
66
  name: "frinus",
67
- version: "3.5.0",
67
+ version: "3.6.1",
68
68
  }, {
69
69
  capabilities: {
70
70
  tools: {},
@@ -39,7 +39,19 @@ Scopes:
39
39
  },
40
40
  content: {
41
41
  type: "string",
42
- description: "The memory content to store",
42
+ // MIRRORS memory/src/config.py::MAX_MEMORY_CONTENT_CHARS, which is the
43
+ // single source of truth and where the derivation is documented.
44
+ // This is a cross-language copy — if the backend constant changes,
45
+ // change it here too (and in the description below). The live value
46
+ // is also served at GET / as limits.memory_content_max_chars.
47
+ maxLength: 16000,
48
+ description: "The memory content to store. HARD LIMIT: 16000 characters — " +
49
+ "content above this is REJECTED (HTTP 422), nothing is saved. " +
50
+ "Check the length before calling. If your text is longer, split " +
51
+ "it into several smaller, self-contained memories (one fact, " +
52
+ "decision or procedure each) and store them one by one; do not " +
53
+ "truncate it into a single call. Smaller memories are also " +
54
+ "retrieved more accurately by semantic search.",
43
55
  },
44
56
  memory_type: {
45
57
  type: "string",
@@ -1654,10 +1666,14 @@ RULES:
1654
1666
  },
1655
1667
  {
1656
1668
  name: "credential_list",
1657
- description: `List all stored credentials for the current user.
1669
+ description: `List every credential you can access in this organization.
1658
1670
 
1659
- Returns credential metadata (ref, type, created_at) without any secret data.
1660
- Use this to see what integrations have stored credentials.`,
1671
+ Covers credentials you OWN and credentials other members SHARED with you.
1672
+ Each entry says which it is: shared ones are marked with who shared them,
1673
+ and owned ones with how many people they reach.
1674
+
1675
+ Returns metadata only (ref, created_at, sharing info) — never secret data.
1676
+ Use a listed ref with 'credential_exec' to actually use the credential.`,
1661
1677
  inputSchema: {
1662
1678
  type: "object",
1663
1679
  properties: {},
@@ -1680,4 +1696,85 @@ This cannot be undone.`,
1680
1696
  required: ["ref"],
1681
1697
  },
1682
1698
  },
1699
+ {
1700
+ name: "credential_share",
1701
+ description: `Share a credential you own with another member of your organization.
1702
+
1703
+ The grant gives that member the SAME access you have: they can run
1704
+ 'credential_exec' with the ref and read the value. There is no reduced
1705
+ "use but never reveal" level, and that is deliberate — 'credential_exec'
1706
+ takes an arbitrary argv, so any such tier would be one command away from
1707
+ being bypassed. Share only with people you would hand the secret to.
1708
+
1709
+ What stays yours alone is MANAGEMENT: only the owner can share, revoke,
1710
+ overwrite or delete a credential. Receiving a share never confers that.
1711
+
1712
+ Identify the recipient by 'email' (usual) or 'user_id' (Keycloak UUID).
1713
+ They must already be an active member of your organization.
1714
+
1715
+ Idempotent: re-sharing with the same person is a no-op, not an error.
1716
+
1717
+ Revoking later stops FUTURE access — it cannot un-read what was already
1718
+ read. If a secret may have been misused, rotate it.`,
1719
+ inputSchema: {
1720
+ type: "object",
1721
+ properties: {
1722
+ ref: {
1723
+ type: "string",
1724
+ description: "Credential reference you own (e.g., 'mysql_prod', 'jira_muza')",
1725
+ },
1726
+ email: {
1727
+ type: "string",
1728
+ description: "Email of the org member who will receive access",
1729
+ },
1730
+ user_id: {
1731
+ type: "string",
1732
+ description: "Keycloak UUID of the member (alternative to email)",
1733
+ },
1734
+ },
1735
+ required: ["ref"],
1736
+ },
1737
+ },
1738
+ {
1739
+ name: "credential_unshare",
1740
+ description: `Revoke a member's access to a credential you own.
1741
+
1742
+ Stops future access only. It does NOT undo what the person already read:
1743
+ if you are revoking because the secret may have leaked, rotate the secret
1744
+ as well — revocation is not a recall.
1745
+
1746
+ Owner-only.`,
1747
+ inputSchema: {
1748
+ type: "object",
1749
+ properties: {
1750
+ ref: {
1751
+ type: "string",
1752
+ description: "Credential reference you own",
1753
+ },
1754
+ user_id: {
1755
+ type: "string",
1756
+ description: "Keycloak UUID of the member whose access is revoked",
1757
+ },
1758
+ },
1759
+ required: ["ref", "user_id"],
1760
+ },
1761
+ },
1762
+ {
1763
+ name: "credential_shares",
1764
+ description: `List who a credential you own is shared with.
1765
+
1766
+ Owner-only: holding a share does not let you see the other recipients.
1767
+ Returns each grantee (email/name when the directory resolves them), who
1768
+ granted it and when. No secret data.`,
1769
+ inputSchema: {
1770
+ type: "object",
1771
+ properties: {
1772
+ ref: {
1773
+ type: "string",
1774
+ description: "Credential reference you own",
1775
+ },
1776
+ },
1777
+ required: ["ref"],
1778
+ },
1779
+ },
1683
1780
  ];
@@ -194,6 +194,30 @@ export function handleApiError(error) {
194
194
  isError: true,
195
195
  };
196
196
  }
197
+ if (status === 422) {
198
+ // Request validation failure. The backend's `detail` carries the ONLY
199
+ // actionable part (e.g. the memory-content size limit: what the ceiling
200
+ // is, what was received, and to split the text into smaller memories).
201
+ // Without this branch the raw axios error propagates as the useless
202
+ // "Request failed with status code 422" and that guidance never reaches
203
+ // the caller — which is the whole point of returning it.
204
+ const detail = error?.response?.data?.detail;
205
+ const text = Array.isArray(detail)
206
+ ? detail
207
+ .map((d) => {
208
+ const field = Array.isArray(d?.loc) ? d.loc.filter((p) => p !== "body").join(".") : null;
209
+ const msg = String(d?.msg ?? "invalid value").replace(/^Value error,\s*/, "");
210
+ return field ? `${field}: ${msg}` : msg;
211
+ })
212
+ .join("\n")
213
+ : typeof detail === "string"
214
+ ? detail
215
+ : "The request was rejected as invalid.";
216
+ return {
217
+ content: [{ type: "text", text: `Validation error (422). Nothing was saved.\n${text}` }],
218
+ isError: true,
219
+ };
220
+ }
197
221
  return null; // Not a known error, let it propagate
198
222
  }
199
223
  // ---------------------------------------------------------------------------
@@ -2027,11 +2051,22 @@ const handlers = {
2027
2051
  }],
2028
2052
  };
2029
2053
  }
2030
- const lines = items.map((c) => `- ${c.integration_type} (created: ${c.created_at})`);
2054
+ // Mark provenance: owned vs shared-with-me. An agent picking a ref
2055
+ // should know whether it may manage it (share/revoke/delete) or only
2056
+ // use it — management is owner-only server-side.
2057
+ const lines = items.map((c) => {
2058
+ if (c.is_owner === false) {
2059
+ return `- ${c.integration_type} (shared with you; owner manages it)`;
2060
+ }
2061
+ const shared = c.share_count > 0
2062
+ ? `, shared with ${c.share_count} member${c.share_count === 1 ? "" : "s"}`
2063
+ : "";
2064
+ return `- ${c.integration_type} (yours, created: ${c.created_at}${shared})`;
2065
+ });
2031
2066
  return {
2032
2067
  content: [{
2033
2068
  type: "text",
2034
- text: `Stored credentials (${items.length}):\n${lines.join("\n")}`,
2069
+ text: `Credentials you can access (${items.length}):\n${lines.join("\n")}\n\nUse a ref with credential_exec to run a command with it injected.`,
2035
2070
  }],
2036
2071
  };
2037
2072
  }
@@ -2068,6 +2103,126 @@ const handlers = {
2068
2103
  throw error;
2069
2104
  }
2070
2105
  },
2106
+ // --- Sharing -------------------------------------------------------------
2107
+ // A share conveys the owner's full access (use + read). No reduced tier
2108
+ // exists on purpose: credential_exec takes an arbitrary argv, so a
2109
+ // "use but never reveal" level would be bypassable and would only give
2110
+ // false assurance. Management stays owner-only, enforced server-side.
2111
+ async credential_share(args, { cpClient }) {
2112
+ const ref = args.ref;
2113
+ const email = args.email;
2114
+ const userId = args.user_id;
2115
+ if (!email && !userId) {
2116
+ return {
2117
+ content: [{
2118
+ type: "text",
2119
+ text: "Provide either 'email' or 'user_id' of the org member to share with.",
2120
+ }],
2121
+ isError: true,
2122
+ };
2123
+ }
2124
+ if (email && userId) {
2125
+ return {
2126
+ content: [{ type: "text", text: "Provide either 'email' or 'user_id', not both." }],
2127
+ isError: true,
2128
+ };
2129
+ }
2130
+ try {
2131
+ const share = await cpClient.shareCredential(ref, (userId ?? email));
2132
+ const who = share?.email || share?.name || share?.grantee_user_id || email || userId;
2133
+ return {
2134
+ content: [{
2135
+ type: "text",
2136
+ text: `Credential '${ref}' is now shared with ${who}.\n` +
2137
+ "They have the same access you do: they can use it and read its value.\n" +
2138
+ "You remain the only one who can share, revoke, overwrite or delete it.\n" +
2139
+ "Revoking later stops future access but cannot un-read what was already read — rotate the secret if it may leak.",
2140
+ }],
2141
+ };
2142
+ }
2143
+ catch (error) {
2144
+ const status = error?.response?.status;
2145
+ const detail = error?.response?.data?.detail;
2146
+ if (status === 404 || status === 400) {
2147
+ return {
2148
+ content: [{ type: "text", text: detail || `Could not share '${ref}'.` }],
2149
+ isError: true,
2150
+ };
2151
+ }
2152
+ const apiErr = handleApiError(error);
2153
+ if (apiErr)
2154
+ return apiErr;
2155
+ throw error;
2156
+ }
2157
+ },
2158
+ async credential_unshare(args, { cpClient }) {
2159
+ const ref = args.ref;
2160
+ const userId = args.user_id;
2161
+ try {
2162
+ await cpClient.revokeCredentialShare(ref, userId);
2163
+ return {
2164
+ content: [{
2165
+ type: "text",
2166
+ text: `Access to '${ref}' revoked for ${userId}.\n` +
2167
+ "This stops FUTURE access only. If the secret may have been read and misused, rotate it.",
2168
+ }],
2169
+ };
2170
+ }
2171
+ catch (error) {
2172
+ if (error?.response?.status === 404) {
2173
+ return {
2174
+ content: [{
2175
+ type: "text",
2176
+ text: error?.response?.data?.detail || `No share found for '${ref}' and that user.`,
2177
+ }],
2178
+ isError: true,
2179
+ };
2180
+ }
2181
+ const apiErr = handleApiError(error);
2182
+ if (apiErr)
2183
+ return apiErr;
2184
+ throw error;
2185
+ }
2186
+ },
2187
+ async credential_shares(args, { cpClient }) {
2188
+ const ref = args.ref;
2189
+ try {
2190
+ const result = await cpClient.listCredentialShares(ref);
2191
+ const items = result?.items || [];
2192
+ if (items.length === 0) {
2193
+ return {
2194
+ content: [{ type: "text", text: `Credential '${ref}' is not shared with anyone.` }],
2195
+ };
2196
+ }
2197
+ const lines = items.map((sh) => {
2198
+ const who = sh.email || sh.name || sh.grantee_user_id;
2199
+ return `- ${who} (user_id: ${sh.grantee_user_id}, since ${sh.granted_at})`;
2200
+ });
2201
+ return {
2202
+ content: [{
2203
+ type: "text",
2204
+ text: `'${ref}' is shared with ${items.length} member${items.length === 1 ? "" : "s"}:\n` +
2205
+ lines.join("\n") +
2206
+ "\n\nEach of them has the same access you do. Use credential_unshare(ref, user_id) to revoke.",
2207
+ }],
2208
+ };
2209
+ }
2210
+ catch (error) {
2211
+ if (error?.response?.status === 404) {
2212
+ return {
2213
+ content: [{
2214
+ type: "text",
2215
+ text: error?.response?.data?.detail || `You do not own a credential named '${ref}'`,
2216
+ }],
2217
+ isError: true,
2218
+ };
2219
+ }
2220
+ const apiErr = handleApiError(error);
2221
+ if (apiErr)
2222
+ return apiErr;
2223
+ throw error;
2224
+ }
2225
+ },
2071
2226
  };
2072
2227
  // ---------------------------------------------------------------------------
2073
2228
  // Dispatcher: looks up the handler and executes it with error handling
@@ -75,6 +75,9 @@ export interface CpClientInterface {
75
75
  getCredential(integrationRef: string): Promise<any>;
76
76
  listCredentials(): Promise<any>;
77
77
  deleteCredential(integrationRef: string): Promise<void>;
78
+ listCredentialShares(integrationRef: string): Promise<any>;
79
+ shareCredential(integrationRef: string, userIdOrEmail: string): Promise<any>;
80
+ revokeCredentialShare(integrationRef: string, granteeUserId: string): Promise<void>;
78
81
  }
79
82
  /** Public surface of the MemoryClient used by handlers. */
80
83
  export interface MemoryClientInterface {
package/manifest.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "frinus",
4
4
  "display_name": "Frinus — Memória Cognitiva",
5
- "version": "3.5.0",
6
- "description": "Memória cognitiva de longo prazo para o Claude (φρήν + νοῦς). 80 ferramentas de memória, sessões, agentes e conhecimento.",
5
+ "version": "3.6.1",
6
+ "description": "Memória cognitiva de longo prazo para o Claude (φρήν + νοῦς). 83 ferramentas de memória, sessões, agentes e conhecimento.",
7
7
  "long_description": "O Frinus dá ao Claude memória de longo prazo, identidade e inteligência persistente entre conversas. Conecta o Claude Desktop à plataforma Frinus (Memory Engine, Control Plane e Agent Service) via MCP. Basta colar sua chave de API pessoal — nada para instalar ou compilar.",
8
8
  "author": {
9
9
  "name": "Frinus",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frinus-mcp",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "description": "Frinus - MCP Server for Cognitive Memory (φρήν + νοῦς). Long-term memory, sessions, agents and knowledge for Claude and any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",