@mindstone-engineering/mcp-server-freshdesk 0.1.0 → 0.2.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/LICENSE +97 -0
- package/README.md +98 -0
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +1 -1
- package/dist/client.js +5 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/tools/configure.js +4 -2
- package/dist/tools/fields.js +1 -1
- package/dist/tools/tickets.js +1 -1
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +23 -0
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Mindstone Engineering
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
**Licensor**: Mindstone Engineering
|
|
18
|
+
|
|
19
|
+
### The Software
|
|
20
|
+
|
|
21
|
+
The "Software" is each version of the software that we make available under
|
|
22
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
23
|
+
Conditions with the Software.
|
|
24
|
+
|
|
25
|
+
**Software**: Freshdesk MCP Server
|
|
26
|
+
|
|
27
|
+
### License Grant
|
|
28
|
+
|
|
29
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
30
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
31
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
32
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
33
|
+
|
|
34
|
+
### Permitted Purpose
|
|
35
|
+
|
|
36
|
+
A Permitted Purpose is any purpose other than a Competing Use. A "Competing
|
|
37
|
+
Use" means making the Software available to third parties as a commercial
|
|
38
|
+
hosted service that directly competes with any product or service provided by
|
|
39
|
+
the Licensor.
|
|
40
|
+
|
|
41
|
+
### Patents
|
|
42
|
+
|
|
43
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
44
|
+
patents, the license grant above includes a license under our patents. If you
|
|
45
|
+
make a claim against any party that the Software infringes or contributes to
|
|
46
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
47
|
+
immediately.
|
|
48
|
+
|
|
49
|
+
### Redistribution
|
|
50
|
+
|
|
51
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
52
|
+
the Software.
|
|
53
|
+
|
|
54
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
55
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
56
|
+
remove any copyright notices provided in or with the Software.
|
|
57
|
+
|
|
58
|
+
### Disclaimer
|
|
59
|
+
|
|
60
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
61
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
62
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
63
|
+
|
|
64
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
65
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, OF
|
|
66
|
+
ANY CHARACTER INCLUDING DAMAGES FOR LOSS OF GOODWILL, LOST PROFITS, LOST SALES
|
|
67
|
+
OR BUSINESS, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, LOST CONTENT,
|
|
68
|
+
DATA OR DATA USE, BREACH OF DUTY OF GOOD FAITH, OR ANY AND ALL OTHER DAMAGES
|
|
69
|
+
OR LOSSES OF ANY KIND OR NATURE WHATSOEVER (WHETHER DIRECT, INDIRECT, SPECIAL,
|
|
70
|
+
COLLATERAL, INCIDENTAL, CONSEQUENTIAL OR OTHERWISE) ARISING OUT OF OR IN
|
|
71
|
+
CONNECTION WITH THE SOFTWARE OR THIS LICENSE, EVEN IF SUCH PARTY SHALL HAVE
|
|
72
|
+
BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
73
|
+
|
|
74
|
+
### Trademark
|
|
75
|
+
|
|
76
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
77
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
78
|
+
trademarks, trade names, service marks or product names.
|
|
79
|
+
|
|
80
|
+
## Change Date
|
|
81
|
+
|
|
82
|
+
Four years from the date the Software is made available under these Terms and
|
|
83
|
+
Conditions: **2030-04-08**
|
|
84
|
+
|
|
85
|
+
## Change License
|
|
86
|
+
|
|
87
|
+
MIT License
|
|
88
|
+
|
|
89
|
+
## License Details
|
|
90
|
+
|
|
91
|
+
| Parameter | Value |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Licensor | Mindstone Engineering |
|
|
94
|
+
| Software | Freshdesk MCP Server |
|
|
95
|
+
| Use Limitation | Competing Use |
|
|
96
|
+
| Change Date | 2030-04-08 |
|
|
97
|
+
| Change License | MIT |
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @mindstone-engineering/mcp-server-freshdesk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@mindstone-engineering/mcp-server-freshdesk)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Freshdesk Support MCP server for Model Context Protocol hosts. Manage helpdesk tickets, search and filter support requests, reply to customers, add internal notes, and configure Freshdesk accounts — all through a standardised MCP interface.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Node.js 20+
|
|
11
|
+
- npm
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Install & build
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd <path-to-repo>/connectors/freshdesk
|
|
19
|
+
npm install
|
|
20
|
+
npm run build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### npx (once published)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @mindstone-engineering/mcp-server-freshdesk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Local
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node dist/index.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
### Environment variables
|
|
38
|
+
|
|
39
|
+
- `FRESHDESK_CONFIG_PATH` — path to the config directory that stores account credentials (defaults to `~/.mcp/freshdesk`)
|
|
40
|
+
- `MCP_HOST_BRIDGE_STATE` — optional path to a host bridge state file used for credential management
|
|
41
|
+
- `MINDSTONE_REBEL_BRIDGE_STATE` — backwards-compatible alias for `MCP_HOST_BRIDGE_STATE`
|
|
42
|
+
|
|
43
|
+
## Host configuration examples
|
|
44
|
+
|
|
45
|
+
### Claude Desktop / Cursor
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"Freshdesk": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@mindstone-engineering/mcp-server-freshdesk"],
|
|
53
|
+
"env": {
|
|
54
|
+
"FRESHDESK_CONFIG_PATH": "~/.mcp/freshdesk"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Local development (no npm publish needed)
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"Freshdesk": {
|
|
67
|
+
"command": "node",
|
|
68
|
+
"args": ["<path-to-repo>/connectors/freshdesk/dist/index.js"],
|
|
69
|
+
"env": {
|
|
70
|
+
"FRESHDESK_CONFIG_PATH": "~/.mcp/freshdesk"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Tools (11)
|
|
78
|
+
|
|
79
|
+
### Account management
|
|
80
|
+
- `configure_freshdesk` — Connect a Freshdesk account using subdomain and API key
|
|
81
|
+
- `list_freshdesk_accounts` — List connected Freshdesk accounts with agent emails
|
|
82
|
+
- `remove_freshdesk_account` — Disconnect a Freshdesk account
|
|
83
|
+
|
|
84
|
+
### Tickets
|
|
85
|
+
- `list_freshdesk_tickets` — List tickets using predefined filters
|
|
86
|
+
- `get_freshdesk_ticket` — Get a single ticket by ID with optional conversations
|
|
87
|
+
- `search_freshdesk_tickets` — Search tickets using Freshdesk query syntax
|
|
88
|
+
- `create_freshdesk_ticket` — Create a new ticket
|
|
89
|
+
- `update_freshdesk_ticket` — Update ticket fields, status, or assignee
|
|
90
|
+
- `reply_to_freshdesk_ticket` — Add a public reply to a ticket
|
|
91
|
+
- `add_freshdesk_note` — Add a private or public note to a ticket
|
|
92
|
+
|
|
93
|
+
### Discovery
|
|
94
|
+
- `list_freshdesk_ticket_fields` — List all ticket fields including custom fields
|
|
95
|
+
|
|
96
|
+
## Licence
|
|
97
|
+
|
|
98
|
+
[FSL-1.1-MIT](./LICENSE) — Functional Source License, Version 1.1, with MIT future licence. The software converts to MIT licence on 2030-04-08.
|
package/dist/bridge.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const BRIDGE_STATE_PATH: string;
|
|
|
5
5
|
/**
|
|
6
6
|
* Send a request to the host app bridge.
|
|
7
7
|
*
|
|
8
|
-
* The bridge is an HTTP server running inside the host app (e.g.
|
|
8
|
+
* The bridge is an HTTP server running inside the host app (e.g. the host application)
|
|
9
9
|
* that handles credential management and other cross-process operations.
|
|
10
10
|
*/
|
|
11
11
|
export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
|
package/dist/bridge.js
CHANGED
|
@@ -18,7 +18,7 @@ const loadBridgeState = () => {
|
|
|
18
18
|
/**
|
|
19
19
|
* Send a request to the host app bridge.
|
|
20
20
|
*
|
|
21
|
-
* The bridge is an HTTP server running inside the host app (e.g.
|
|
21
|
+
* The bridge is an HTTP server running inside the host app (e.g. the host application)
|
|
22
22
|
* that handles credential management and other cross-process operations.
|
|
23
23
|
*/
|
|
24
24
|
export const bridgeRequest = async (urlPath, body) => {
|
package/dist/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Base URL: https://{domain}.freshdesk.com/api/v2
|
|
9
9
|
*/
|
|
10
10
|
import { FreshdeskError, REQUEST_TIMEOUT_MS } from './types.js';
|
|
11
|
+
import { validateSubdomain } from './utils.js';
|
|
11
12
|
/**
|
|
12
13
|
* Make an authenticated request to the Freshdesk API.
|
|
13
14
|
*
|
|
@@ -18,6 +19,8 @@ import { FreshdeskError, REQUEST_TIMEOUT_MS } from './types.js';
|
|
|
18
19
|
* @returns Parsed JSON response
|
|
19
20
|
*/
|
|
20
21
|
export async function freshdeskFetch(domain, apiKey, endpoint, options = {}) {
|
|
22
|
+
// Defence-in-depth: validate domain before URL construction
|
|
23
|
+
validateSubdomain(domain);
|
|
21
24
|
const { params, ...fetchOptions } = options;
|
|
22
25
|
// Build URL with query params
|
|
23
26
|
let url = `https://${domain}.freshdesk.com/api/v2${endpoint}`;
|
|
@@ -63,7 +66,7 @@ export async function freshdeskFetch(domain, apiKey, endpoint, options = {}) {
|
|
|
63
66
|
}
|
|
64
67
|
// Handle auth errors
|
|
65
68
|
if (response.status === 401) {
|
|
66
|
-
throw new FreshdeskError('Authentication failed', 'AUTH_FAILED', 'API key is invalid or revoked. Check your Freshdesk API key
|
|
69
|
+
throw new FreshdeskError('Authentication failed', 'AUTH_FAILED', 'API key is invalid or revoked. Check your Freshdesk API key in your MCP host\'s settings.');
|
|
67
70
|
}
|
|
68
71
|
// Handle forbidden
|
|
69
72
|
if (response.status === 403) {
|
|
@@ -82,7 +85,7 @@ export async function freshdeskFetch(domain, apiKey, endpoint, options = {}) {
|
|
|
82
85
|
: response.status >= 500
|
|
83
86
|
? 'Freshdesk server error - try again later'
|
|
84
87
|
: 'Request failed';
|
|
85
|
-
throw new FreshdeskError(`Freshdesk API error (${response.status}): ${statusMessage}`, 'API_ERROR', 'Check the request parameters and try again. If the problem persists, reconnect your Freshdesk account in
|
|
88
|
+
throw new FreshdeskError(`Freshdesk API error (${response.status}): ${statusMessage}`, 'API_ERROR', 'Check the request parameters and try again. If the problem persists, reconnect your Freshdesk account in your MCP host\'s settings.');
|
|
86
89
|
}
|
|
87
90
|
// Handle 204 No Content
|
|
88
91
|
if (response.status === 204) {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Environment variables:
|
|
9
9
|
* - FRESHDESK_CONFIG_PATH: Path to config directory containing accounts.json
|
|
10
|
-
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
11
|
-
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
10
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (primary, optional)
|
|
11
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy/deprecated bridge state path (optional)
|
|
12
12
|
*/
|
|
13
13
|
export {};
|
|
14
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Environment variables:
|
|
9
9
|
* - FRESHDESK_CONFIG_PATH: Path to config directory containing accounts.json
|
|
10
|
-
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
11
|
-
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
10
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (primary, optional)
|
|
11
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy/deprecated bridge state path (optional)
|
|
12
12
|
*/
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
14
|
import { createServer } from './server.js';
|
package/dist/tools/configure.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { loadAccounts, getAccountsConfig, removeAccount, upsertAccount } from '../auth.js';
|
|
3
3
|
import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
|
|
4
4
|
import { FreshdeskError } from '../types.js';
|
|
5
|
-
import { withErrorHandling } from '../utils.js';
|
|
5
|
+
import { withErrorHandling, validateSubdomain } from '../utils.js';
|
|
6
6
|
export function registerConfigureTools(server) {
|
|
7
7
|
// ── configure_freshdesk ─────────────────────────────────────────
|
|
8
8
|
server.registerTool('configure_freshdesk', {
|
|
@@ -22,6 +22,8 @@ export function registerConfigureTools(server) {
|
|
|
22
22
|
}, withErrorHandling(async (args) => {
|
|
23
23
|
const domain = args.domain.trim();
|
|
24
24
|
const apiKey = args.api_key.trim();
|
|
25
|
+
// Validate subdomain before any storage or network call
|
|
26
|
+
validateSubdomain(domain);
|
|
25
27
|
// If bridge is available, persist via bridge
|
|
26
28
|
if (BRIDGE_STATE_PATH) {
|
|
27
29
|
try {
|
|
@@ -71,7 +73,7 @@ export function registerConfigureTools(server) {
|
|
|
71
73
|
return JSON.stringify({
|
|
72
74
|
ok: true,
|
|
73
75
|
accounts: [],
|
|
74
|
-
message: 'No Freshdesk accounts connected. Use configure_freshdesk
|
|
76
|
+
message: 'No Freshdesk accounts connected. Use configure_freshdesk to connect your account.',
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
const accountList = config.accounts.map((account) => ({
|
package/dist/tools/fields.js
CHANGED
|
@@ -21,7 +21,7 @@ export function registerFieldTools(server) {
|
|
|
21
21
|
return JSON.stringify({
|
|
22
22
|
ok: false,
|
|
23
23
|
error: 'No Freshdesk account connected',
|
|
24
|
-
resolution: 'Use configure_freshdesk
|
|
24
|
+
resolution: 'Use configure_freshdesk to connect your account.',
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
const fields = await freshdeskFetch(account.domain, account.apiKey, '/admin/ticket_fields');
|
package/dist/tools/tickets.js
CHANGED
|
@@ -8,7 +8,7 @@ function noAccountError() {
|
|
|
8
8
|
return JSON.stringify({
|
|
9
9
|
ok: false,
|
|
10
10
|
error: 'No Freshdesk account connected',
|
|
11
|
-
resolution: 'Use configure_freshdesk
|
|
11
|
+
resolution: 'Use configure_freshdesk to connect your account.',
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
export function registerTicketTools(server) {
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates that a Freshdesk subdomain is a safe, single-label hostname component.
|
|
4
|
+
*
|
|
5
|
+
* Rejects any domain containing `/`, `@`, `?`, `#`, `.`, `%`, whitespace, or
|
|
6
|
+
* other characters that could redirect API requests to an attacker-controlled host.
|
|
7
|
+
*
|
|
8
|
+
* @throws {FreshdeskError} with code INVALID_SUBDOMAIN if validation fails
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateSubdomain(domain: string): void;
|
|
2
11
|
type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
|
|
3
12
|
/**
|
|
4
13
|
* Wraps a tool handler with standard error handling.
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { FreshdeskError } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Strict subdomain regex: only lowercase alphanumerics and hyphens,
|
|
4
|
+
* must start and end with an alphanumeric character.
|
|
5
|
+
* Prevents API key exfiltration via URL manipulation.
|
|
6
|
+
*/
|
|
7
|
+
const SUBDOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
8
|
+
/**
|
|
9
|
+
* Validates that a Freshdesk subdomain is a safe, single-label hostname component.
|
|
10
|
+
*
|
|
11
|
+
* Rejects any domain containing `/`, `@`, `?`, `#`, `.`, `%`, whitespace, or
|
|
12
|
+
* other characters that could redirect API requests to an attacker-controlled host.
|
|
13
|
+
*
|
|
14
|
+
* @throws {FreshdeskError} with code INVALID_SUBDOMAIN if validation fails
|
|
15
|
+
*/
|
|
16
|
+
export function validateSubdomain(domain) {
|
|
17
|
+
if (!domain || !domain.trim()) {
|
|
18
|
+
throw new FreshdeskError('Freshdesk subdomain cannot be empty', 'INVALID_SUBDOMAIN', 'Provide a valid Freshdesk subdomain (e.g. "acme" for acme.freshdesk.com).');
|
|
19
|
+
}
|
|
20
|
+
const trimmed = domain.trim();
|
|
21
|
+
if (!SUBDOMAIN_RE.test(trimmed)) {
|
|
22
|
+
throw new FreshdeskError(`Invalid Freshdesk subdomain: "${trimmed}". Only lowercase alphanumeric characters and hyphens are allowed.`, 'INVALID_SUBDOMAIN', 'Provide just the subdomain part (e.g. "acme" for acme.freshdesk.com). Do not include the full URL, dots, or special characters.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* Wraps a tool handler with standard error handling.
|
|
4
27
|
*
|