@mgsoftwarebv/mcp-server-bridge 2.15.1 → 2.17.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/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { ListToolsRequestSchema, ListResourcesRequestSchema, CallToolRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { createClient } from '@supabase/supabase-js';
6
6
  import { createHash } from 'crypto';
7
+ import { Octokit } from '@octokit/rest';
7
8
 
8
9
  var args = process.argv.slice(2);
9
10
  var apiKey = args.find((arg) => arg.startsWith("--api-key="))?.split("=")[1] || process.env.MG_TICKETS_API_KEY;
@@ -67,6 +68,99 @@ async function downloadImageAsBase64(storageKey) {
67
68
  return null;
68
69
  }
69
70
  }
71
+ async function getGithubTokenForProject(projectId, teamId) {
72
+ try {
73
+ const { data: repoData, error: repoError } = await supabase.from("project_github_repositories").select("repository_full_name").eq("project_id", projectId).eq("team_id", teamId).single();
74
+ if (repoError || !repoData) {
75
+ console.error(`No GitHub repository linked to project ${projectId}`);
76
+ return null;
77
+ }
78
+ const { data: appData, error: appError } = await supabase.from("apps").select("config").eq("team_id", teamId).eq("app_id", "github").single();
79
+ if (appError || !appData?.config?.access_token) {
80
+ console.error(`GitHub app not connected for team ${teamId}`);
81
+ return null;
82
+ }
83
+ const accessToken = appData.config.access_token;
84
+ const repositoryFullName = repoData.repository_full_name;
85
+ const [owner, repo] = repositoryFullName.split("/");
86
+ if (!owner || !repo) {
87
+ console.error(`Invalid repository full name: ${repositoryFullName}`);
88
+ return null;
89
+ }
90
+ return {
91
+ token: accessToken,
92
+ repositoryFullName,
93
+ owner,
94
+ repo
95
+ };
96
+ } catch (error) {
97
+ console.error("Error getting GitHub token for project:", error);
98
+ return null;
99
+ }
100
+ }
101
+ async function transitionToNextPhase(sessionId, currentPhase) {
102
+ try {
103
+ const now = /* @__PURE__ */ new Date();
104
+ const phaseOrder = ["analysis", "bug_investigation", "development", "communication"];
105
+ const { data: allPhases, error: fetchError } = await supabase.from("ai_time_logs").select("*").eq("ai_session_id", sessionId).order("activity_type");
106
+ if (fetchError || !allPhases) {
107
+ console.error("Failed to fetch phases for transition:", fetchError);
108
+ return;
109
+ }
110
+ let currentPhaseType = currentPhase;
111
+ if (!currentPhaseType) {
112
+ const activePhase = allPhases.find((p) => p.status === "in_progress");
113
+ currentPhaseType = activePhase?.activity_type;
114
+ }
115
+ if (!currentPhaseType) {
116
+ const analysisPhase = allPhases.find((p) => p.activity_type === "analysis");
117
+ if (analysisPhase && analysisPhase.status === "pending" && analysisPhase.estimated_duration_seconds > 0) {
118
+ await supabase.from("ai_time_logs").update({
119
+ status: "in_progress",
120
+ started_at: now.toISOString()
121
+ }).eq("id", analysisPhase.id);
122
+ console.error("\u2705 Started analysis phase");
123
+ }
124
+ return;
125
+ }
126
+ const currentPhaseRecord = allPhases.find((p) => p.activity_type === currentPhaseType && p.status === "in_progress");
127
+ if (currentPhaseRecord) {
128
+ const duration = Math.round((now.getTime() - new Date(currentPhaseRecord.started_at).getTime()) / 1e3);
129
+ await supabase.from("ai_time_logs").update({
130
+ status: "completed",
131
+ ended_at: now.toISOString(),
132
+ duration_seconds: duration
133
+ }).eq("id", currentPhaseRecord.id);
134
+ console.error(`\u2705 Completed phase: ${currentPhaseType} (${duration}s)`);
135
+ }
136
+ const currentIndex = phaseOrder.indexOf(currentPhaseType);
137
+ if (currentIndex === -1 || currentIndex === phaseOrder.length - 1) {
138
+ console.error("No next phase to transition to");
139
+ return;
140
+ }
141
+ for (let i = currentIndex + 1; i < phaseOrder.length; i++) {
142
+ const nextPhaseType = phaseOrder[i];
143
+ const nextPhase = allPhases.find((p) => p.activity_type === nextPhaseType);
144
+ if (!nextPhase) continue;
145
+ if (nextPhase.estimated_duration_seconds === 0) {
146
+ await supabase.from("ai_time_logs").update({ status: "skipped" }).eq("id", nextPhase.id);
147
+ console.error(`\u23ED\uFE0F Skipped phase: ${nextPhaseType} (0 minutes estimated)`);
148
+ continue;
149
+ }
150
+ if (nextPhase.status === "pending") {
151
+ await supabase.from("ai_time_logs").update({
152
+ status: "in_progress",
153
+ started_at: now.toISOString()
154
+ }).eq("id", nextPhase.id);
155
+ console.error(`\u2705 Started next phase: ${nextPhaseType}`);
156
+ return;
157
+ }
158
+ }
159
+ console.error("All remaining phases skipped or completed");
160
+ } catch (error) {
161
+ console.error("Error transitioning to next phase:", error);
162
+ }
163
+ }
70
164
  var server = new Server(
71
165
  {
72
166
  name: "mg-tickets-mcp-bridge",
@@ -416,6 +510,113 @@ var TOOLS = [
416
510
  },
417
511
  required: ["workDescription", "estimatedHours"]
418
512
  }
513
+ },
514
+ // === GITHUB TOOLS ===
515
+ {
516
+ name: "search-github-code",
517
+ description: "Search for code in a GitHub repository. Use this to find relevant files, functions, or code patterns related to a ticket.",
518
+ inputSchema: {
519
+ type: "object",
520
+ properties: {
521
+ projectId: {
522
+ type: "string",
523
+ description: "Project ID (UUID) - Required to identify which GitHub repository to search"
524
+ },
525
+ query: {
526
+ type: "string",
527
+ description: "Search query (e.g., function name, class name, error message, component name)"
528
+ },
529
+ language: {
530
+ type: "string",
531
+ description: 'Optional: Filter by programming language (e.g., "typescript", "python", "javascript")'
532
+ },
533
+ path: {
534
+ type: "string",
535
+ description: 'Optional: Filter by file path pattern (e.g., "src/components")'
536
+ },
537
+ maxResults: {
538
+ type: "number",
539
+ default: 10,
540
+ maximum: 30,
541
+ description: "Maximum number of results to return (default: 10, max: 30)"
542
+ }
543
+ },
544
+ required: ["projectId", "query"]
545
+ }
546
+ },
547
+ {
548
+ name: "get-github-file",
549
+ description: "Get the contents of a specific file from a GitHub repository. Use this after finding relevant files to read their full content.",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ projectId: {
554
+ type: "string",
555
+ description: "Project ID (UUID)"
556
+ },
557
+ filePath: {
558
+ type: "string",
559
+ description: 'Full path to the file in the repository (e.g., "src/components/Button.tsx")'
560
+ },
561
+ ref: {
562
+ type: "string",
563
+ description: "Optional: Git reference (branch, tag, or commit SHA). Defaults to repository default branch."
564
+ }
565
+ },
566
+ required: ["projectId", "filePath"]
567
+ }
568
+ },
569
+ {
570
+ name: "list-github-directory",
571
+ description: "List files and directories in a GitHub repository directory. Use this to explore repository structure.",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ projectId: {
576
+ type: "string",
577
+ description: "Project ID (UUID)"
578
+ },
579
+ directoryPath: {
580
+ type: "string",
581
+ description: 'Path to directory (e.g., "src/components"). Use empty string or "/" for root directory.'
582
+ },
583
+ ref: {
584
+ type: "string",
585
+ description: "Optional: Git reference (branch, tag, or commit SHA). Defaults to repository default branch."
586
+ }
587
+ },
588
+ required: ["projectId", "directoryPath"]
589
+ }
590
+ },
591
+ {
592
+ name: "search-github-issues",
593
+ description: "Search for GitHub issues and pull requests related to a topic. Useful to find similar past issues or relevant discussions.",
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ projectId: {
598
+ type: "string",
599
+ description: "Project ID (UUID)"
600
+ },
601
+ query: {
602
+ type: "string",
603
+ description: "Search query for issues/PRs"
604
+ },
605
+ state: {
606
+ type: "string",
607
+ enum: ["open", "closed", "all"],
608
+ default: "all",
609
+ description: "Filter by issue state"
610
+ },
611
+ maxResults: {
612
+ type: "number",
613
+ default: 10,
614
+ maximum: 30,
615
+ description: "Maximum number of results to return"
616
+ }
617
+ },
618
+ required: ["projectId", "query"]
619
+ }
419
620
  }
420
621
  ];
421
622
  var RESOURCES = [
@@ -444,24 +645,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
444
645
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
445
646
  return { resources: RESOURCES };
446
647
  });
447
- async function updatePhaseTransition(sessionId, fromPhase, toPhase) {
448
- const now = (/* @__PURE__ */ new Date()).toISOString();
449
- const { data: prevPhase } = await supabase.from("ai_time_logs").select("*").eq("ai_session_id", sessionId).eq("activity_type", fromPhase).eq("status", "in_progress").single();
450
- if (prevPhase) {
451
- const duration = Math.round(
452
- (new Date(now).getTime() - new Date(prevPhase.started_at).getTime()) / 1e3
453
- );
454
- await supabase.from("ai_time_logs").update({
455
- ended_at: now,
456
- duration_seconds: duration,
457
- status: "completed"
458
- }).eq("id", prevPhase.id);
459
- }
460
- await supabase.from("ai_time_logs").update({
461
- started_at: now,
462
- status: "in_progress"
463
- }).eq("ai_session_id", sessionId).eq("activity_type", toPhase).eq("status", "pending");
464
- }
465
648
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
466
649
  if (!authContext) {
467
650
  return {
@@ -1021,12 +1204,19 @@ ${context.ticketData ? `Ticket: ${context.ticketData.ticket_number} - ${context.
1021
1204
  const allCompleted = currentTodos.every((t) => t.status === "completed");
1022
1205
  const { data: currentPhase } = await supabase.from("ai_time_logs").select("activity_type, status").eq("ai_session_id", session.id).eq("status", "in_progress").single();
1023
1206
  if (hasInProgress && currentPhase?.activity_type === "analysis") {
1024
- await updatePhaseTransition(session.id, "analysis", "development");
1025
- phaseTransition = "Analysis \u2192 Development";
1207
+ await transitionToNextPhase(session.id, "analysis");
1208
+ phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
1209
+ }
1210
+ if (hasInProgress && currentPhase?.activity_type === "bug_investigation") {
1211
+ const completedCount = currentTodos.filter((t) => t.status === "completed").length;
1212
+ if (completedCount > 0) {
1213
+ await transitionToNextPhase(session.id, "bug_investigation");
1214
+ phaseTransition = "Investigation completed \u2192 Development phase started";
1215
+ }
1026
1216
  }
1027
1217
  if (allCompleted && currentPhase?.activity_type === "development") {
1028
- await updatePhaseTransition(session.id, "development", "communication");
1029
- phaseTransition = "Development \u2192 Communication";
1218
+ await transitionToNextPhase(session.id, "development");
1219
+ phaseTransition = "Development completed \u2192 Communication phase started";
1030
1220
  }
1031
1221
  }
1032
1222
  return {
@@ -1471,6 +1661,297 @@ ${efficiencyNotes}
1471
1661
  }]
1472
1662
  };
1473
1663
  }
1664
+ // === GITHUB TOOLS ===
1665
+ case "search-github-code": {
1666
+ const { projectId, query, language, path, maxResults = 10 } = args2;
1667
+ const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1668
+ if (!githubInfo) {
1669
+ return {
1670
+ content: [{
1671
+ type: "text",
1672
+ text: "\u274C GitHub not configured for this project. Please:\n1. Connect GitHub app for your team\n2. Link a GitHub repository to this project"
1673
+ }]
1674
+ };
1675
+ }
1676
+ try {
1677
+ const octokit = new Octokit({ auth: githubInfo.token });
1678
+ let searchQuery = `${query} repo:${githubInfo.repositoryFullName}`;
1679
+ if (language) searchQuery += ` language:${language}`;
1680
+ if (path) searchQuery += ` path:${path}`;
1681
+ console.error(`\u{1F50D} Searching GitHub: ${searchQuery}`);
1682
+ const { data } = await octokit.rest.search.code({
1683
+ q: searchQuery,
1684
+ per_page: Math.min(maxResults, 30)
1685
+ });
1686
+ if (!data.items || data.items.length === 0) {
1687
+ return {
1688
+ content: [{
1689
+ type: "text",
1690
+ text: `\u{1F50D} No code found matching "${query}" in ${githubInfo.repositoryFullName}`
1691
+ }]
1692
+ };
1693
+ }
1694
+ let responseText = `\u{1F50D} **Found ${data.items.length} code matches in ${githubInfo.repositoryFullName}:**
1695
+
1696
+ `;
1697
+ for (const item of data.items) {
1698
+ responseText += `\u{1F4C4} **${item.path}**
1699
+ `;
1700
+ responseText += ` Repository: ${item.repository.full_name}
1701
+ `;
1702
+ responseText += ` URL: ${item.html_url}
1703
+ `;
1704
+ if (item.score) responseText += ` Relevance: ${item.score.toFixed(2)}
1705
+ `;
1706
+ responseText += `
1707
+ `;
1708
+ }
1709
+ responseText += `
1710
+ \u{1F4A1} Use \`get-github-file\` to read the full content of any file.`;
1711
+ return {
1712
+ content: [{
1713
+ type: "text",
1714
+ text: responseText
1715
+ }]
1716
+ };
1717
+ } catch (error) {
1718
+ console.error("GitHub search error:", error);
1719
+ if (error.status === 403 && error.message?.includes("rate limit")) {
1720
+ return {
1721
+ content: [{
1722
+ type: "text",
1723
+ text: "\u26A0\uFE0F GitHub API rate limit exceeded. Please try again later."
1724
+ }]
1725
+ };
1726
+ }
1727
+ if (error.status === 401) {
1728
+ return {
1729
+ content: [{
1730
+ type: "text",
1731
+ text: "\u274C GitHub token invalid. Please reconnect the GitHub app."
1732
+ }]
1733
+ };
1734
+ }
1735
+ return {
1736
+ content: [{
1737
+ type: "text",
1738
+ text: `\u274C GitHub search failed: ${error.message || "Unknown error"}`
1739
+ }]
1740
+ };
1741
+ }
1742
+ }
1743
+ case "get-github-file": {
1744
+ const { projectId, filePath, ref } = args2;
1745
+ const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1746
+ if (!githubInfo) {
1747
+ return {
1748
+ content: [{
1749
+ type: "text",
1750
+ text: "\u274C GitHub not configured for this project."
1751
+ }]
1752
+ };
1753
+ }
1754
+ try {
1755
+ const octokit = new Octokit({ auth: githubInfo.token });
1756
+ console.error(`\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`);
1757
+ const { data } = await octokit.rest.repos.getContent({
1758
+ owner: githubInfo.owner,
1759
+ repo: githubInfo.repo,
1760
+ path: filePath,
1761
+ ref
1762
+ });
1763
+ if (Array.isArray(data) || data.type !== "file") {
1764
+ return {
1765
+ content: [{
1766
+ type: "text",
1767
+ text: `\u274C "${filePath}" is not a file or contains multiple items.`
1768
+ }]
1769
+ };
1770
+ }
1771
+ const content = Buffer.from(data.content, "base64").toString("utf-8");
1772
+ let responseText = `\u{1F4C4} **File: ${filePath}**
1773
+ `;
1774
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1775
+ `;
1776
+ responseText += `Size: ${data.size} bytes
1777
+ `;
1778
+ responseText += `URL: ${data.html_url}
1779
+
1780
+ `;
1781
+ responseText += `**Content:**
1782
+ \`\`\`
1783
+ ${content}
1784
+ \`\`\``;
1785
+ return {
1786
+ content: [{
1787
+ type: "text",
1788
+ text: responseText
1789
+ }]
1790
+ };
1791
+ } catch (error) {
1792
+ console.error("GitHub get file error:", error);
1793
+ if (error.status === 404) {
1794
+ return {
1795
+ content: [{
1796
+ type: "text",
1797
+ text: `\u274C File not found: ${filePath}`
1798
+ }]
1799
+ };
1800
+ }
1801
+ return {
1802
+ content: [{
1803
+ type: "text",
1804
+ text: `\u274C Failed to read file: ${error.message || "Unknown error"}`
1805
+ }]
1806
+ };
1807
+ }
1808
+ }
1809
+ case "list-github-directory": {
1810
+ const { projectId, directoryPath, ref } = args2;
1811
+ const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1812
+ if (!githubInfo) {
1813
+ return {
1814
+ content: [{
1815
+ type: "text",
1816
+ text: "\u274C GitHub not configured for this project."
1817
+ }]
1818
+ };
1819
+ }
1820
+ try {
1821
+ const octokit = new Octokit({ auth: githubInfo.token });
1822
+ const normalizedPath = !directoryPath || directoryPath === "/" ? "" : directoryPath;
1823
+ console.error(`\u{1F4C1} Listing directory: ${normalizedPath || "(root)"} in ${githubInfo.repositoryFullName}`);
1824
+ const { data } = await octokit.rest.repos.getContent({
1825
+ owner: githubInfo.owner,
1826
+ repo: githubInfo.repo,
1827
+ path: normalizedPath,
1828
+ ref
1829
+ });
1830
+ if (!Array.isArray(data)) {
1831
+ return {
1832
+ content: [{
1833
+ type: "text",
1834
+ text: `\u274C "${directoryPath}" is not a directory.`
1835
+ }]
1836
+ };
1837
+ }
1838
+ let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
1839
+ `;
1840
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1841
+ `;
1842
+ responseText += `Items: ${data.length}
1843
+
1844
+ `;
1845
+ const directories = data.filter((item) => item.type === "dir");
1846
+ const files = data.filter((item) => item.type === "file");
1847
+ if (directories.length > 0) {
1848
+ responseText += `**\u{1F4C1} Directories (${directories.length}):**
1849
+ `;
1850
+ for (const dir of directories) {
1851
+ responseText += ` - ${dir.name}/
1852
+ `;
1853
+ }
1854
+ responseText += `
1855
+ `;
1856
+ }
1857
+ if (files.length > 0) {
1858
+ responseText += `**\u{1F4C4} Files (${files.length}):**
1859
+ `;
1860
+ for (const file of files) {
1861
+ responseText += ` - ${file.name} (${file.size} bytes)
1862
+ `;
1863
+ }
1864
+ }
1865
+ return {
1866
+ content: [{
1867
+ type: "text",
1868
+ text: responseText
1869
+ }]
1870
+ };
1871
+ } catch (error) {
1872
+ console.error("GitHub list directory error:", error);
1873
+ if (error.status === 404) {
1874
+ return {
1875
+ content: [{
1876
+ type: "text",
1877
+ text: `\u274C Directory not found: ${directoryPath}`
1878
+ }]
1879
+ };
1880
+ }
1881
+ return {
1882
+ content: [{
1883
+ type: "text",
1884
+ text: `\u274C Failed to list directory: ${error.message || "Unknown error"}`
1885
+ }]
1886
+ };
1887
+ }
1888
+ }
1889
+ case "search-github-issues": {
1890
+ const { projectId, query, state = "all", maxResults = 10 } = args2;
1891
+ const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1892
+ if (!githubInfo) {
1893
+ return {
1894
+ content: [{
1895
+ type: "text",
1896
+ text: "\u274C GitHub not configured for this project."
1897
+ }]
1898
+ };
1899
+ }
1900
+ try {
1901
+ const octokit = new Octokit({ auth: githubInfo.token });
1902
+ let searchQuery = `${query} repo:${githubInfo.repositoryFullName} is:issue`;
1903
+ if (state !== "all") searchQuery += ` state:${state}`;
1904
+ console.error(`\u{1F50D} Searching GitHub issues: ${searchQuery}`);
1905
+ const { data } = await octokit.rest.search.issuesAndPullRequests({
1906
+ q: searchQuery,
1907
+ per_page: Math.min(maxResults, 30)
1908
+ });
1909
+ if (!data.items || data.items.length === 0) {
1910
+ return {
1911
+ content: [{
1912
+ type: "text",
1913
+ text: `\u{1F50D} No issues found matching "${query}" in ${githubInfo.repositoryFullName}`
1914
+ }]
1915
+ };
1916
+ }
1917
+ let responseText = `\u{1F50D} **Found ${data.items.length} issues in ${githubInfo.repositoryFullName}:**
1918
+
1919
+ `;
1920
+ for (const issue of data.items) {
1921
+ const isPR = !!issue.pull_request;
1922
+ const emoji = isPR ? "\u{1F500}" : "\u{1F41B}";
1923
+ const type = isPR ? "PR" : "Issue";
1924
+ responseText += `${emoji} **${type} #${issue.number}: ${issue.title}**
1925
+ `;
1926
+ responseText += ` State: ${issue.state}
1927
+ `;
1928
+ responseText += ` Author: ${issue.user?.login || "Unknown"}
1929
+ `;
1930
+ responseText += ` URL: ${issue.html_url}
1931
+ `;
1932
+ if (issue.labels && issue.labels.length > 0) {
1933
+ responseText += ` Labels: ${issue.labels.map((l) => typeof l === "string" ? l : l.name).join(", ")}
1934
+ `;
1935
+ }
1936
+ responseText += `
1937
+ `;
1938
+ }
1939
+ return {
1940
+ content: [{
1941
+ type: "text",
1942
+ text: responseText
1943
+ }]
1944
+ };
1945
+ } catch (error) {
1946
+ console.error("GitHub search issues error:", error);
1947
+ return {
1948
+ content: [{
1949
+ type: "text",
1950
+ text: `\u274C Failed to search issues: ${error.message || "Unknown error"}`
1951
+ }]
1952
+ };
1953
+ }
1954
+ }
1474
1955
  default:
1475
1956
  throw new Error(`Unknown tool: ${name}`);
1476
1957
  }