itfin-mcp 0.1.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/README.md +83 -0
- package/dist/clock.js +1 -0
- package/dist/config.js +14 -0
- package/dist/errors.js +10 -0
- package/dist/index.js +22 -0
- package/dist/itfinClient.js +79 -0
- package/dist/jwt.js +10 -0
- package/dist/leave.js +56 -0
- package/dist/login.js +122 -0
- package/dist/reminders.js +61 -0
- package/dist/server.js +557 -0
- package/dist/tokenStore.js +40 -0
- package/dist/workingTime.js +28 -0
- package/package.json +51 -0
- package/scripts/install.sh +188 -0
- package/scripts/pick-clients.mjs +30 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "itfin-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local MCP server for ITFin: projects, time entries and ITFin token lifecycle",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"itfin",
|
|
8
|
+
"claude",
|
|
9
|
+
"codex",
|
|
10
|
+
"timesheet"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/steven-tailor/itfin-mcp.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/steven-tailor/itfin-mcp#readme",
|
|
17
|
+
"bugs": "https://github.com/steven-tailor/itfin-mcp/issues",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"itfin-mcp": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"scripts"
|
|
25
|
+
],
|
|
26
|
+
"os": [
|
|
27
|
+
"darwin"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"start": "node dist/index.js",
|
|
34
|
+
"prepack": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@inquirer/checkbox": "^5.2.5",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
42
|
+
"luxon": "^3.7.2",
|
|
43
|
+
"zod": "^4.6.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/luxon": "^3.7.5",
|
|
47
|
+
"@types/node": "^26.6.2",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^5.0.1"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Quick install for itfin-mcp: installs or updates the npm package in ~/.itfin-mcp and registers the
|
|
3
|
+
# MCP server with Claude Code, Codex and Claude Desktop. Usage:
|
|
4
|
+
# npx itfin-mcp install [--company acme] [--clients claude,codex,desktop] [--browser chrome] [--work-start 10:00] [--timezone Europe/Kyiv]
|
|
5
|
+
# curl -fsSL https://raw.githubusercontent.com/steven-tailor/itfin-mcp/main/scripts/install.sh | bash -s -- [same flags]
|
|
6
|
+
# The first install asks which apps to install for and the company name. Updates reuse the apps and
|
|
7
|
+
# settings of the previous install.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
DIR="${ITFIN_MCP_DIR:-$HOME/.itfin-mcp}"
|
|
11
|
+
# What npm installs. Override it to try a local build: ITFIN_MCP_PACKAGE=./itfin-mcp-0.1.0.tgz
|
|
12
|
+
PACKAGE="${ITFIN_MCP_PACKAGE:-itfin-mcp@latest}"
|
|
13
|
+
CODEX_CONFIG="${CODEX_HOME:-$HOME/.codex}/config.toml"
|
|
14
|
+
DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
15
|
+
URL="" BROWSER="" WORK_START="" TIMEZONE="" CLIENTS=""
|
|
16
|
+
|
|
17
|
+
while [[ $# -gt 0 ]]; do
|
|
18
|
+
case "$1" in
|
|
19
|
+
--company) URL="https://$2.itfin.io"; shift 2 ;;
|
|
20
|
+
--url) URL="$2"; shift 2 ;;
|
|
21
|
+
--clients) CLIENTS="${2//,/ }"; shift 2 ;;
|
|
22
|
+
--browser) BROWSER="$2"; shift 2 ;;
|
|
23
|
+
--work-start) WORK_START="$2"; shift 2 ;;
|
|
24
|
+
--timezone) TIMEZONE="$2"; shift 2 ;;
|
|
25
|
+
--dir) DIR="$2"; shift 2 ;;
|
|
26
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
fail() { echo "✗ $*" >&2; exit 1; }
|
|
31
|
+
# Read answers from the terminal even when the script itself comes from a pipe.
|
|
32
|
+
ask() { local prompt="$1" default="${2:-}" answer; read -r -p "$prompt${default:+ [$default]}: " answer </dev/tty || true; echo "${answer:-$default}"; }
|
|
33
|
+
|
|
34
|
+
[[ "$(uname)" == "Darwin" ]] || fail "itfin-mcp needs macOS (Keychain, launchd and notifications)."
|
|
35
|
+
command -v node >/dev/null || fail "Node.js 22+ is required: https://nodejs.org"
|
|
36
|
+
[[ "$(node -p 'process.versions.node.split(".")[0]')" -ge 22 ]] || fail "Node.js 22+ is required (found $(node -v))."
|
|
37
|
+
command -v npm >/dev/null || fail "npm is required. It comes with Node.js: https://nodejs.org"
|
|
38
|
+
|
|
39
|
+
PKG="$DIR/node_modules/itfin-mcp"
|
|
40
|
+
version() { node -p 'require(process.argv[1]).version' "$PKG/package.json" 2>/dev/null || true; }
|
|
41
|
+
OLD_VERSION=""
|
|
42
|
+
if [[ -d "$DIR/.git" ]]; then
|
|
43
|
+
# Installs before the npm package were a git clone of the repo, which npm can't install into.
|
|
44
|
+
[[ "$(git -C "$DIR" remote get-url origin 2>/dev/null)" == *itfin-mcp* ]] || fail "$DIR is a git repo that isn't itfin-mcp. Pick another folder with --dir."
|
|
45
|
+
echo "→ Replacing the git clone in $DIR with the npm package"
|
|
46
|
+
OLD_VERSION="git"
|
|
47
|
+
rm -rf "$DIR"
|
|
48
|
+
else
|
|
49
|
+
OLD_VERSION="$(version)"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "→ Installing $PACKAGE from npm"
|
|
53
|
+
mkdir -p "$DIR"
|
|
54
|
+
npm install --prefix "$DIR" --silent --no-audit --no-fund --omit=dev "$PACKAGE"
|
|
55
|
+
NEW_VERSION="$(version)"
|
|
56
|
+
[[ -n "$NEW_VERSION" ]] || fail "npm did not install itfin-mcp into $DIR."
|
|
57
|
+
|
|
58
|
+
# Servers that apps started before this update keep running the old code until they exit. Stop them,
|
|
59
|
+
# matching only the node processes (not the wrappers apps launch them with), so no app keeps using
|
|
60
|
+
# stale tools. Each app starts a fresh server for its next session, or after a restart.
|
|
61
|
+
# The pattern also matches the dist/index.js of a replaced git clone.
|
|
62
|
+
STALE=""
|
|
63
|
+
[[ -n "$OLD_VERSION" && "$OLD_VERSION" != "$NEW_VERSION" ]] && STALE="$(pgrep -f "^[^ ]*node $DIR/(node_modules/itfin-mcp/)?dist/index\.js\$" || true)"
|
|
64
|
+
if [[ -n "$STALE" ]]; then
|
|
65
|
+
echo "→ Stopping $(echo "$STALE" | wc -l | tr -d ' ') running itfin-mcp server(s) with the old code"
|
|
66
|
+
kill $STALE 2>/dev/null || true
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Apps to register with: claude (Claude Code), codex, desktop (Claude Desktop).
|
|
70
|
+
# Updates keep the apps of the previous install. A first install asks, with every app found checked.
|
|
71
|
+
if [[ -z "$CLIENTS" ]]; then
|
|
72
|
+
command -v claude >/dev/null && claude mcp get itfin >/dev/null 2>&1 && CLIENTS+="claude "
|
|
73
|
+
[[ -f "$CODEX_CONFIG" ]] && grep -q '^\[mcp_servers\.itfin\]$' "$CODEX_CONFIG" && CLIENTS+="codex "
|
|
74
|
+
[[ -f "$DESKTOP_CONFIG" ]] && grep -q '"itfin"' "$DESKTOP_CONFIG" && CLIENTS+="desktop "
|
|
75
|
+
fi
|
|
76
|
+
if [[ -z "$CLIENTS" ]]; then
|
|
77
|
+
FOUND=()
|
|
78
|
+
command -v claude >/dev/null && FOUND+=("claude=Claude Code")
|
|
79
|
+
command -v codex >/dev/null && FOUND+=("codex=Codex")
|
|
80
|
+
[[ -d /Applications/Claude.app || -f "$DESKTOP_CONFIG" ]] && FOUND+=("desktop=Claude Desktop")
|
|
81
|
+
[[ ${#FOUND[@]} -gt 0 ]] || fail "Install Claude Code, Codex or Claude Desktop first."
|
|
82
|
+
if (: </dev/tty) 2>/dev/null; then
|
|
83
|
+
CLIENTS="$(node "$PKG/scripts/pick-clients.mjs" "${FOUND[@]}")" || fail "Cancelled."
|
|
84
|
+
else
|
|
85
|
+
for found in "${FOUND[@]}"; do CLIENTS+="${found%%=*} "; done
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
CLIENTS=" $(echo $CLIENTS) "
|
|
89
|
+
for client in $CLIENTS; do
|
|
90
|
+
[[ "$client" =~ ^(claude|codex|desktop)$ ]] || fail "Unknown app: $client. Use claude, codex or desktop."
|
|
91
|
+
done
|
|
92
|
+
wants() { [[ "$CLIENTS" == *" $1 "* ]]; }
|
|
93
|
+
|
|
94
|
+
# Settings of the previous install, from the Claude Code registration, the Codex config or the
|
|
95
|
+
# Claude Desktop config.
|
|
96
|
+
PREV=""
|
|
97
|
+
if command -v claude >/dev/null; then PREV+="$(claude mcp get itfin 2>/dev/null || true)"$'\n'; fi
|
|
98
|
+
if [[ -f "$CODEX_CONFIG" ]]; then PREV+="$(cat "$CODEX_CONFIG")"$'\n'; fi
|
|
99
|
+
if [[ -f "$DESKTOP_CONFIG" ]]; then
|
|
100
|
+
PREV+="$(node -e 'const env = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).mcpServers?.itfin?.env ?? {};
|
|
101
|
+
for (const [k, v] of Object.entries(env)) console.log(`${k}=${v}`)' "$DESKTOP_CONFIG" 2>/dev/null || true)"
|
|
102
|
+
fi
|
|
103
|
+
# Prints the value of KEY from lines like `KEY=value` or `KEY = "value"`.
|
|
104
|
+
prev() {
|
|
105
|
+
printf '%s\n' "$PREV" | awk -v k="$1" '{
|
|
106
|
+
sub(/^[ \t]+/, "")
|
|
107
|
+
if (index($0, k) != 1) next
|
|
108
|
+
rest = substr($0, length(k) + 1)
|
|
109
|
+
if (rest !~ /^[ \t]*=/) next
|
|
110
|
+
sub(/^[ \t]*=[ \t]*/, "", rest); gsub(/"/, "", rest); sub(/[ \t]+$/, "", rest)
|
|
111
|
+
print rest; exit
|
|
112
|
+
}'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[[ -n "$URL" ]] || URL="$(prev ITFIN_URL)"
|
|
116
|
+
[[ -n "$BROWSER" ]] || BROWSER="$(prev ITFIN_BROWSER)"
|
|
117
|
+
[[ -n "$WORK_START" ]] || WORK_START="$(prev ITFIN_WORK_START)"
|
|
118
|
+
[[ -n "$TIMEZONE" ]] || TIMEZONE="$(prev ITFIN_TIMEZONE)"
|
|
119
|
+
|
|
120
|
+
if [[ -z "$URL" ]]; then
|
|
121
|
+
COMPANY="$(ask "Company name in ITFin (acme for https://acme.itfin.io)")"
|
|
122
|
+
COMPANY="$(printf '%s' "$COMPANY" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
123
|
+
# Also accept a pasted address like https://acme.itfin.io.
|
|
124
|
+
COMPANY="${COMPANY#*://}"; COMPANY="${COMPANY%%.itfin.io*}"
|
|
125
|
+
[[ "$COMPANY" =~ ^[a-z0-9-]+$ ]] || fail "Company name must be letters, digits or dashes, like acme."
|
|
126
|
+
URL="https://$COMPANY.itfin.io"
|
|
127
|
+
fi
|
|
128
|
+
URL="${URL%/}"
|
|
129
|
+
[[ "$URL" =~ ^https?://[^/]+$ ]] || fail "Workspace address must look like https://acme.itfin.io"
|
|
130
|
+
echo "→ Workspace: $URL"
|
|
131
|
+
|
|
132
|
+
ENTRY="$PKG/dist/index.js"
|
|
133
|
+
# Absolute path, because Claude Desktop does not see version-manager shims on PATH.
|
|
134
|
+
NODE="$(command -v node)"
|
|
135
|
+
# Settings left empty are not passed, so the server uses its defaults (chrome, 10:00, system timezone).
|
|
136
|
+
ENV_PAIRS=()
|
|
137
|
+
for pair in "ITFIN_URL=$URL" "ITFIN_BROWSER=$BROWSER" "ITFIN_WORK_START=$WORK_START" "ITFIN_TIMEZONE=$TIMEZONE"; do
|
|
138
|
+
[[ -n "${pair#*=}" ]] && ENV_PAIRS+=("$pair")
|
|
139
|
+
done
|
|
140
|
+
ENV_ARGS=()
|
|
141
|
+
for pair in "${ENV_PAIRS[@]}"; do ENV_ARGS+=(-e "$pair"); done
|
|
142
|
+
|
|
143
|
+
if wants claude; then
|
|
144
|
+
command -v claude >/dev/null || fail "The claude CLI was not found."
|
|
145
|
+
echo "→ Registering the MCP server with Claude Code (user scope)"
|
|
146
|
+
claude mcp remove itfin --scope user >/dev/null 2>&1 || true
|
|
147
|
+
claude mcp add itfin --scope user "${ENV_ARGS[@]}" -- "$NODE" "$ENTRY"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if wants codex; then
|
|
151
|
+
command -v codex >/dev/null || fail "The codex CLI was not found."
|
|
152
|
+
echo "→ Registering the MCP server with Codex"
|
|
153
|
+
codex mcp remove itfin >/dev/null 2>&1 || true
|
|
154
|
+
codex mcp add itfin "${ENV_ARGS[@]/#-e/--env}" -- "$NODE" "$ENTRY"
|
|
155
|
+
# itfin_login waits up to 3 minutes; Codex gives up on tool calls after 60 seconds by default.
|
|
156
|
+
sed -i '' '/^\[mcp_servers\.itfin\]$/a\
|
|
157
|
+
tool_timeout_sec = 240
|
|
158
|
+
' "$CODEX_CONFIG"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
DESKTOP=""
|
|
162
|
+
if wants desktop; then
|
|
163
|
+
echo "→ Registering the MCP server with Claude Desktop"
|
|
164
|
+
# Replaces only mcpServers.itfin and keeps the rest of the file. The old file is kept as .bak.
|
|
165
|
+
if node -e '
|
|
166
|
+
const fs = require("fs");
|
|
167
|
+
const [file, command, entry, ...pairs] = process.argv.slice(1);
|
|
168
|
+
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
169
|
+
const config = text.trim() ? JSON.parse(text) : {};
|
|
170
|
+
if (text) fs.writeFileSync(file + ".bak", text);
|
|
171
|
+
const env = Object.fromEntries(pairs.map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]));
|
|
172
|
+
config.mcpServers = { ...config.mcpServers, itfin: { command, args: [entry], env } };
|
|
173
|
+
fs.mkdirSync(require("path").dirname(file), { recursive: true });
|
|
174
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
175
|
+
' "$DESKTOP_CONFIG" "$NODE" "$ENTRY" "${ENV_PAIRS[@]}"; then
|
|
176
|
+
DESKTOP=1
|
|
177
|
+
else
|
|
178
|
+
echo "! Could not update $DESKTOP_CONFIG (is it valid JSON?), so Claude Desktop was not registered."
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
echo
|
|
183
|
+
echo "✓ itfin-mcp $NEW_VERSION is installed in $DIR"
|
|
184
|
+
# Claude Desktop reads its config only at startup. Not quit here: it may be running this script.
|
|
185
|
+
[[ -n "$DESKTOP" ]] && echo "Restart Claude Desktop (Cmd+Q, then open it) to load the server."
|
|
186
|
+
[[ -n "${STALE:-}" ]] && echo "Open sessions lost the ITFin tools: start a new session, or reconnect itfin with /mcp in Claude Code."
|
|
187
|
+
echo "Next: start a new session and ask it to \"log in to ITFin\"."
|
|
188
|
+
echo "Run npx itfin-mcp@latest install at any time to update."
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Checkbox list for install.sh: which apps to register the MCP server with.
|
|
2
|
+
// Usage: node pick-clients.mjs id=Label ... Prints the chosen ids, one per line.
|
|
3
|
+
// Talks to /dev/tty, because install.sh itself usually comes from a pipe (curl | bash).
|
|
4
|
+
import { openSync } from "node:fs";
|
|
5
|
+
import { ReadStream, WriteStream } from "node:tty";
|
|
6
|
+
import checkbox from "@inquirer/checkbox";
|
|
7
|
+
|
|
8
|
+
const input = new ReadStream(openSync("/dev/tty", "r"));
|
|
9
|
+
const output = new WriteStream(openSync("/dev/tty", "w"));
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const chosen = await checkbox(
|
|
13
|
+
{
|
|
14
|
+
message: "Install for",
|
|
15
|
+
choices: process.argv.slice(2).map((arg) => {
|
|
16
|
+
const [value, name] = arg.split("=");
|
|
17
|
+
return { value, name, checked: true };
|
|
18
|
+
}),
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
{ input, output },
|
|
22
|
+
);
|
|
23
|
+
console.log(chosen.join("\n"));
|
|
24
|
+
} catch {
|
|
25
|
+
// Ctrl+C: print nothing, install.sh stops.
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
} finally {
|
|
28
|
+
input.destroy();
|
|
29
|
+
output.destroy();
|
|
30
|
+
}
|